/*
 * #%L
 * Alfresco Repository
 * %%
 * Copyright (C) 2005 - 2023 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */

package org.alfresco.repo.rendition;

import static org.alfresco.repo.rendition2.TestSynchronousTransformClient.TEST_LONG_RUNNING_MIME_TYPE;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import javax.imageio.ImageIO;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.transaction.TestTransaction;
import org.springframework.transaction.annotation.Transactional;

import org.alfresco.model.ContentModel;
import org.alfresco.model.RenditionModel;
import org.alfresco.repo.action.RuntimeActionService;
import org.alfresco.repo.action.executer.ExporterActionExecuter;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.content.transform.AbstractContentTransformerTest;
import org.alfresco.repo.content.transform.UnimportantTransformException;
import org.alfresco.repo.content.transform.magick.ImageTransformationOptions;
import org.alfresco.repo.jscript.ClasspathScriptLocation;
import org.alfresco.repo.model.Repository;
import org.alfresco.repo.policy.BehaviourFilter;
import org.alfresco.repo.rendition.executer.AbstractRenderingEngine;
import org.alfresco.repo.rendition.executer.FreemarkerRenderingEngine;
import org.alfresco.repo.rendition.executer.ImageRenderingEngine;
import org.alfresco.repo.rendition.executer.ReformatRenderingEngine;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.cmr.action.Action;
import org.alfresco.service.cmr.coci.CheckOutCheckInService;
import org.alfresco.service.cmr.lock.LockService;
import org.alfresco.service.cmr.lock.LockType;
import org.alfresco.service.cmr.rendition.CompositeRenditionDefinition;
import org.alfresco.service.cmr.rendition.RenderCallback;
import org.alfresco.service.cmr.rendition.RenderingEngineDefinition;
import org.alfresco.service.cmr.rendition.RenditionCancelledException;
import org.alfresco.service.cmr.rendition.RenditionDefinition;
import org.alfresco.service.cmr.rendition.RenditionService;
import org.alfresco.service.cmr.rendition.RenditionServiceException;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentData;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.CropSourceOptions.CropSourceOptionsSerializer;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.ScriptLocation;
import org.alfresco.service.cmr.repository.ScriptService;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.RegexQNamePattern;
import org.alfresco.test_category.OwnJVMTestsCategory;
import org.alfresco.util.BaseAlfrescoSpringTest;
import org.alfresco.util.GUID;
import org.alfresco.util.Pair;

/**
 * @author Neil McErlean
 * @author Nick Smith
 * @since 3.3
 *
 * @deprecated We are introducing the new async RenditionService2.
 */
@Deprecated
@SuppressWarnings("deprecation")
@Category({OwnJVMTestsCategory.class})
@Transactional
@ContextConfiguration({"classpath:alfresco/application-context.xml",
        "classpath:org/alfresco/repo/rendition2/test-transform-context.xml"})
public class RenditionServiceIntegrationTest extends BaseAlfrescoSpringTest
{
    private static final String WHITE = "ffffff";
    private static final String BLACK = "000000";
    private final static QName REFORMAT_RENDER_DEFN_NAME = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI,
            ReformatRenderingEngine.NAME + System.currentTimeMillis());
    private final static QName RESCALE_RENDER_DEFN_NAME = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI,
            ImageRenderingEngine.NAME + System.currentTimeMillis());
    private final static String QUICK_CONTENT = "The quick brown fox jumps over the lazy dog";
    private final static String FM_TEMPLATE = "/org/alfresco/repo/rendition/renditionTestTemplate.ftl";

    private NodeRef nodeWithDocContent;
    private NodeRef nodeWithImageContent;
    private NodeRef nodeWithFreeMarkerContent;
    private NodeRef testTargetFolder;

    private NodeRef renditionNode = null;

    private NamespaceService namespaceService;
    private RenditionService renditionService;
    private Repository repositoryHelper;
    private ScriptService scriptService;
    private RetryingTransactionHelper transactionHelper;

    @Before
    public void before() throws Exception
    {
        super.before();
        this.namespaceService = (NamespaceService) this.applicationContext.getBean("namespaceService");
        this.renditionService = (RenditionService) this.applicationContext.getBean("renditionService");
        this.repositoryHelper = (Repository) this.applicationContext.getBean("repositoryHelper");
        this.scriptService = (ScriptService) this.applicationContext.getBean("scriptService");
        this.transactionHelper = (RetryingTransactionHelper) this.applicationContext
                .getBean("retryingTransactionHelper");

        // Set the current security context as admin
        AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName());

        // Register our test rendering engine if needed
        DummyHelloWorldRenditionEngine.registerIfNeeded(this.applicationContext);

        // Create test folders
        NodeRef companyHome = this.repositoryHelper.getCompanyHome();

        // Create the test folder used for these tests
        this.testTargetFolder = createNode(companyHome, "testFolder-" + GUID.generate(), ContentModel.TYPE_FOLDER);

        // Create the node used as a content supplier for tests
        this.nodeWithDocContent = createContentNode(companyHome, "testDocContent-" + GUID.generate());

        // Put some known PDF content in it.
        File pdfQuickFile = AbstractContentTransformerTest.loadQuickTestFile("pdf");
        assertNotNull("Failed to load required test file.", pdfQuickFile);

        nodeService.setProperty(nodeWithDocContent, ContentModel.PROP_CONTENT, new ContentData(null,
                MimetypeMap.MIMETYPE_PDF, 0L, null));
        ContentWriter writer = contentService.getWriter(nodeWithDocContent, ContentModel.PROP_CONTENT, true);
        writer.setMimetype(MimetypeMap.MIMETYPE_PDF);
        writer.setEncoding("UTF-8");
        writer.putContent(pdfQuickFile);

        // Put the titled aspect on it - used for testing rendition updates
        Map<QName, Serializable> titledProps = new HashMap<QName, Serializable>();
        titledProps.put(ContentModel.PROP_TITLE, "Original test title");
        titledProps.put(ContentModel.PROP_DESCRIPTION, "Dummy description");
        nodeService.addAspect(nodeWithDocContent, ContentModel.ASPECT_TITLED, titledProps);

        // Create a test image
        this.nodeWithImageContent = createContentNode(companyHome, "testImageNode-" + GUID.generate());
        // Stream some well-known image content into the node.
        URL url = RenditionServiceIntegrationTest.class.getClassLoader().getResource("images/gray21.512.png");
        assertNotNull("url of test image was null", url);
        File imageFile = new File(url.getFile());
        setImageContentOnNode(nodeWithImageContent, MimetypeMap.MIMETYPE_IMAGE_PNG, imageFile);

        // Create a test template node.
        this.nodeWithFreeMarkerContent = createFreeMarkerNode(companyHome);
        TestTransaction.flagForCommit();
        TestTransaction.end();
        TestTransaction.start();
    }

    private NodeRef createContentNode(NodeRef companyHome, String name)
    {
        return createNode(companyHome, name, ContentModel.TYPE_CONTENT);
    }

    private void setImageContentOnNode(NodeRef nodeWithImage, String mimetypeImage, File imageFile)
    {
        assertTrue(imageFile.exists());
        nodeService.setProperty(nodeWithImage, ContentModel.PROP_CONTENT, new ContentData(null,
                mimetypeImage, 0L, null));
        ContentWriter writer = contentService.getWriter(nodeWithImage, ContentModel.PROP_CONTENT, true);
        writer.setMimetype(mimetypeImage);
        writer.setEncoding("UTF-8");
        writer.putContent(imageFile);
    }

    private NodeRef createFreeMarkerNode(NodeRef companyHome)
    {
        NodeRef fmNode = createContentNode(companyHome, "testFreeMarkerNode-" + GUID.generate());
        nodeService.setProperty(fmNode, ContentModel.PROP_CONTENT, new ContentData(null,
                MimetypeMap.MIMETYPE_TEXT_PLAIN, 0L, null));

        URL url = getClass().getResource(FM_TEMPLATE);
        assertNotNull("The url is null", url);
        File templateFile = new File(url.getFile());
        assertTrue("The template file does not exist", templateFile.exists());

        ContentWriter fmWriter = contentService.getWriter(fmNode, ContentModel.PROP_CONTENT, true);
        fmWriter.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN);
        fmWriter.setEncoding("UTF-8");
        fmWriter.putContent(templateFile);
        return fmNode;
    }

    @After
    public void after() throws Exception
    {
        super.after();
    }

    @Test
    public void testRenderFreeMarkerTemplate() throws Exception
    {
        final QName renditionName = QName.createQName(NamespaceService.RENDITION_MODEL_1_0_URI,
                FreemarkerRenderingEngine.NAME);

        this.renditionNode = transactionHelper
                .doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<NodeRef>() {
                    @Override
                    public NodeRef execute() throws Throwable
                    {
                        // create test model
                        RenditionDefinition definition = renditionService.createRenditionDefinition(renditionName,
                                FreemarkerRenderingEngine.NAME);
                        definition.setParameterValue(FreemarkerRenderingEngine.PARAM_TEMPLATE_NODE,
                                nodeWithFreeMarkerContent);
                        ChildAssociationRef renditionAssoc = renditionService
                                .render(nodeWithDocContent, definition);
                        assertNotNull("The rendition association was null", renditionAssoc);
                        return renditionAssoc.getChildRef();
                    }
                });

        transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>() {
            @Override
            public Void execute() throws Throwable
            {
                String output = readTextContent(renditionNode);
                assertNotNull("The rendition content was null.", output);
                // check the output contains root node Id as expected.
                assertTrue(output.contains(nodeWithDocContent.getId()));
                return null;
            }
        });
    }

    @Test
    public void testHTMLRenderFreeMarkerTemplate() throws Exception
    {
        final QName renditionName = QName.createQName(NamespaceService.RENDITION_MODEL_1_0_URI,
                "htmlRenderingDefinition");

        int numberOfItemsInTrashcanBeforeRendition = getNumberOfItemsInTrashcan();
        this.renditionNode = transactionHelper
                .doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<NodeRef>() {
                    @Override
                    public NodeRef execute() throws Throwable
                    {
                        // create test model
                        RenditionDefinition htmlDefinition = renditionService
                                .createRenditionDefinition(renditionName,
                                        FreemarkerRenderingEngine.NAME);

                        htmlDefinition.setParameterValue(
                                FreemarkerRenderingEngine.PARAM_TEMPLATE_NODE,
                                nodeWithFreeMarkerContent);
                        htmlDefinition.setParameterValue(FreemarkerRenderingEngine.PARAM_MIME_TYPE,
                                MimetypeMap.MIMETYPE_HTML);

                        Map<String, Serializable> paramMap = new HashMap<String, Serializable>();
                        paramMap.put("test", "test");
                        htmlDefinition.setParameterValue(FreemarkerRenderingEngine.PARAM_MODEL,
                                (Serializable) paramMap);

                        ChildAssociationRef renditionAssoc = renditionService.render(
                                nodeWithDocContent, htmlDefinition);
                        assertNotNull("The rendition association was null", renditionAssoc);
                        return renditionAssoc.getChildRef();
                    }
                });

        // testcase for MNT-14386
        int numberOfItemsInTrashcanAfterRendition = getNumberOfItemsInTrashcan();
        assertEquals(numberOfItemsInTrashcanBeforeRendition, numberOfItemsInTrashcanAfterRendition);

    }

    /**
     * This method returns the number of items from trashcan
     * 
     * @return int
     */
    private int getNumberOfItemsInTrashcan()
    {
        NodeRef archiveRootNodeRef = nodeService.getRootNode(StoreRef.STORE_REF_ARCHIVE_SPACESSTORE);
        return nodeService.countChildAssocs(archiveRootNodeRef, true);

    }

    @Test
    public void testRenderFreeMarkerTemplateOneTransaction() throws Exception
    {
        final QName renditionName = QName.createQName(NamespaceService.RENDITION_MODEL_1_0_URI,
                FreemarkerRenderingEngine.NAME);

        this.renditionNode = transactionHelper
                .doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<NodeRef>() {
                    public NodeRef execute() throws Throwable
                    {
                        // create test model
                        RenditionDefinition definition = renditionService.createRenditionDefinition(renditionName,
                                FreemarkerRenderingEngine.NAME);
                        definition.setParameterValue(FreemarkerRenderingEngine.PARAM_TEMPLATE_NODE,
                                nodeWithFreeMarkerContent);
                        ChildAssociationRef renditionAssoc = renditionService
                                .render(nodeWithDocContent, definition);
                        assertNotNull("The rendition association was null", renditionAssoc);
                        String output = readTextContent(renditionAssoc.getChildRef());
                        assertNotNull("The rendition content was null.", output);
                        // check the output contains root node Id as expected.
                        assertTrue(output.contains(nodeWithDocContent.getId()));
                        return null;
                    }
                });
    }

    @Test
    public void testRenderFreemarkerTemplatePath() throws Exception
    {
        // TODO displayName paths.
        final QName renditionName1 = QName.createQName(NamespaceService.RENDITION_MODEL_1_0_URI,
                FreemarkerRenderingEngine.NAME + "_UpdateOnAnyPropChange");
        final QName renditionName2 = QName.createQName(NamespaceService.RENDITION_MODEL_1_0_URI,
                FreemarkerRenderingEngine.NAME + "_UpdateOnContentPropChange");

        final Pair<NodeRef, NodeRef> renditions = transactionHelper
                .doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Pair<NodeRef, NodeRef>>() {
                    public Pair<NodeRef, NodeRef> execute() throws Throwable
                    {
                        ChildAssociationRef parentAssoc = nodeService.getPrimaryParent(nodeWithFreeMarkerContent);
                        QName assocName = parentAssoc.getQName();
                        String templatePath = "/app:company_home/" + assocName.toPrefixString(namespaceService);

                        // create test model 1 - rendition to update on any property change
                        RenditionDefinition definition1 = renditionService.createRenditionDefinition(renditionName1,
                                FreemarkerRenderingEngine.NAME);
                        definition1.setParameterValue(FreemarkerRenderingEngine.PARAM_TEMPLATE_PATH,
                                templatePath);
                        definition1.setParameterValue(AbstractRenderingEngine.PARAM_UPDATE_RENDITIONS_ON_ANY_PROPERTY_CHANGE, Boolean.TRUE);

                        // create test model 2 - rendition to update on content property change
                        RenditionDefinition definition2 = renditionService.createRenditionDefinition(renditionName2,
                                FreemarkerRenderingEngine.NAME);
                        definition2.setParameterValue(FreemarkerRenderingEngine.PARAM_TEMPLATE_PATH,
                                templatePath);
                        definition2.setParameterValue(AbstractRenderingEngine.PARAM_UPDATE_RENDITIONS_ON_ANY_PROPERTY_CHANGE, Boolean.FALSE);

                        // We need to save these renditions in order to have them eligible
                        // for automatic update.
                        if (null == renditionService.loadRenditionDefinition(renditionName1))
                        {
                            renditionService.saveRenditionDefinition(definition1);
                        }
                        if (null == renditionService.loadRenditionDefinition(renditionName2))
                        {
                            renditionService.saveRenditionDefinition(definition2);
                        }

                        ChildAssociationRef renditionAssoc1 = renditionService
                                .render(nodeWithDocContent, definition1);
                        assertNotNull("The rendition association was null", renditionAssoc1);

                        ChildAssociationRef renditionAssoc2 = renditionService
                                .render(nodeWithDocContent, definition2);
                        assertNotNull("The rendition association was null", renditionAssoc2);

                        Pair<NodeRef, NodeRef> result = new Pair<NodeRef, NodeRef>(renditionAssoc1.getChildRef(), renditionAssoc2.getChildRef());
                        return result;
                    }
                });

        final String titleInitialValue = "Original test title";
        transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>() {
            public Void execute() throws Throwable
            {
                assertEquals("The source node should have title " + titleInitialValue, titleInitialValue,
                        nodeService.getProperty(nodeWithDocContent, ContentModel.PROP_TITLE));

                String output1 = readTextContent(renditions.getFirst());
                assertNotNull("The rendition content was null.", output1);

                assertRenditionContainsTitle(titleInitialValue,
                        output1);

                // check the output contains root node Id as expected.
                assertTrue(output1.contains(nodeWithDocContent.getId()));

                String output2 = readTextContent(renditions.getSecond());
                assertNotNull("The rendition content was null.", output2);

                assertRenditionContainsTitle(titleInitialValue,
                        output2);

                return null;
            }
        });

        // Now change some properties on the source node and ensure that the renditions
        // are updated appropriately
        final String updatedTitle = "updatedTitle";
        transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>() {
            public Void execute() throws Throwable
            {
                nodeService.setProperty(nodeWithDocContent, ContentModel.PROP_TITLE, updatedTitle);
                return null;
            }
        });

        RetryingTransactionCallback<Void> checkRenditionCallback = new RetryingTransactionCallback<Void>() {
            public Void execute() throws Throwable
            {
                String output1 = readTextContent(renditions.getFirst());
                assertNotNull("The rendition content was null.", output1);

                assertRenditionContainsTitle(updatedTitle, output1);

                String output2 = readTextContent(renditions.getSecond());
                assertNotNull("The rendition content was null.", output2);

                assertRenditionContainsTitle(titleInitialValue, output2);

                return null;
            }
        };
        // Sleep to let the asynchronous action queue perform the updates to the renditions.
        int count = 0;
        while (count < 60)
        {
            count++;
            Thread.sleep(1000L);
            // Get the renditions and check their content for the new title
            try
            {
                transactionHelper.doInTransaction(checkRenditionCallback);
                // Success
                break;
            }
            catch (Exception e)
            {
                // Failure
                if (count > 60)
                {
                    throw e;
                }
            }
        }

        transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>() {
            public Void execute() throws Throwable
            {
                renditionService.render(nodeWithDocContent, renditionName1, new RenderCallback() {
                    @Override
                    public void handleSuccessfulRendition(ChildAssociationRef primaryParentOfNewRendition)
                    {
                        assertNotNull("The rendition association was null", primaryParentOfNewRendition);
                    }

                    @Override
                    public void handleFailedRendition(Throwable t)
                    {
                        fail("No error should be thrown: " + t.getMessage());
                    }
                });
                return null;
            }
        });
    }

    private void assertRenditionContainsTitle(final String titleValue, String output)
    {
        final String titleMarker = "TestTitle=";
        final String beforeAfterMarker = "xxx";
        int indexOfTitleMarker = output.indexOf(titleMarker);
        int indexOfStartMarker = output.indexOf(beforeAfterMarker, indexOfTitleMarker);
        int indexOfEndMarker = output.indexOf(beforeAfterMarker, indexOfStartMarker + beforeAfterMarker.length());
        final String titleAsTakenFromRendition = output.substring(indexOfStartMarker + beforeAfterMarker.length(), indexOfEndMarker);

        assertEquals("The rendition should contain title " + titleValue, titleValue, titleAsTakenFromRendition);
    }

    /**
     * This test method uses the RenditionService to render a test document (of type PDF) into a different format (of type plain_text) and place the rendition under the source node.
     */
    @Test
    public void testRenderDocumentInAnotherFormatInSitu() throws Exception
    {
        this.renditionNode = transactionHelper
                .doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<NodeRef>() {
                    public NodeRef execute() throws Throwable
                    {
                        // Initially the node that provides the content
                        // should not have the rn:renditioned aspect on it.
                        assertFalse("Source node has unexpected renditioned aspect.", nodeService.hasAspect(
                                nodeWithDocContent, RenditionModel.ASPECT_RENDITIONED));
                        // and no renditions
                        assertTrue("Renditions should have been empty", renditionService.getRenditions(
                                nodeWithDocContent).isEmpty());
                        assertNull("Renditions should have been null", renditionService.getRenditionByName(
                                nodeWithDocContent, REFORMAT_RENDER_DEFN_NAME));

                        validateRenderingActionDefinition(ReformatRenderingEngine.NAME);

                        RenditionDefinition action = makeReformatAction(null, MimetypeMap.MIMETYPE_TEXT_PLAIN);

                        // Render the content and put the result underneath
                        // the content node
                        ChildAssociationRef renditionAssoc = renditionService.render(nodeWithDocContent, action);
                        NodeRef rendition = renditionAssoc.getChildRef();
                        assertEquals("The parent node was not correct", nodeWithDocContent, renditionAssoc
                                .getParentRef());
                        validateRenditionAssociation(renditionAssoc, REFORMAT_RENDER_DEFN_NAME);

                        // The rendition node should have no other
                        // parent-associations - in this case
                        assertEquals("Wrong value for rendition node parent count.",
                                1, nodeService.getParentAssocs(rendition).size());

                        // Now the source content node should have the
                        // renditioned aspect
                        assertTrue("Source node is missing renditioned aspect.", nodeService.hasAspect(
                                nodeWithDocContent, RenditionModel.ASPECT_RENDITIONED));
                        // and one rendition

                        assertEquals("Renditions size wrong", 1, renditionService.getRenditions(nodeWithDocContent)
                                .size());
                        assertNotNull("Renditions should not have been null", renditionService.getRenditionByName(
                                nodeWithDocContent, REFORMAT_RENDER_DEFN_NAME));

                        return rendition;
                    }
                });

        // Now in a separate transaction, we'll check that the reformatted
        // content is actually there.
        assertNotNull("The rendition node was null.", renditionNode);

        transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>() {
            public Void execute() throws Throwable
            {
                String contentAsString = readTextContent(renditionNode);
                assertTrue("Wrong content in rendition", contentAsString.contains(QUICK_CONTENT));
                return null;
            }
        });
    }

    /**
     * Abstract base implementation of <code>Runnable</code> which sleeps for a short time to allow transformations to start then attempts some modification of the node provided by a concrete implementation and records whether or not the modification thread was blocked.
     * 
     * @see <a href="https://issues.alfresco.com/jira/browse/MNT-9094">MNT-9094</a>
     */
    protected abstract class AbstractNodeModifyingRunnable implements Runnable
    {
        protected Throwable modificationException;
        protected NodeRef nodeRef;

        public AbstractNodeModifyingRunnable(NodeRef nodeRef)
        {
            this.nodeRef = nodeRef;
        }

        public Throwable getModificationException()
        {
            return modificationException;
        }

        protected abstract void modifyNode(NodeRef nodeRef);

        @Override
        public void run()
        {
            RetryingTransactionCallback<Void> txnWork = new RetryingTransactionCallback<Void>() {
                @Override
                public Void execute() throws Throwable
                {
                    // Wait a sufficient time for content transformer to start
                    Thread.sleep(2000);
                    modifyNode(nodeRef);
                    return null;
                }
            };
            // MNT-9598: RenditionServiceIntegrationTest
            // We have to give the transaction reties a chance i.e. DO NOT ABSORB EXCEPTIONS inside transaction.
            // Retrying is standard behaviour so absorbing the exception but still testing for it is guaranteeing failure.
            try
            {
                transactionHelper.doInTransaction(txnWork);
            }
            catch (Exception e)
            {
                modificationException = e;
            }
        }
    }

    /**
     * Utility method which executes the given <code>nodeModifyingRunnable</code> in a new thread then starts a long-running transformation rendition which blocks modification of the node unless cancelled.
     * 
     * @see <a href="https://issues.alfresco.com/jira/browse/MNT-9094">MNT-9094</a>
     * @param nodeModifyingRunnable
     *            the node modification to test
     * @throws Exception
     */
    protected void renderPdfDocumentLongRunningTest(AbstractNodeModifyingRunnable nodeModifyingRunnable) throws Exception
    {
        renderPdfDocumentLongRunningTest(nodeModifyingRunnable, false);
    }

    protected void renderPdfDocumentLongRunningTest(AbstractNodeModifyingRunnable nodeModifyingRunnable, boolean joinNodeModifyingThread) throws Exception
    {
        // Spawn a new thread that waits a bit then tries to modify the node
        Thread nodeModifyingThread = new Thread(nodeModifyingRunnable);
        nodeModifyingThread.start();

        // Start the long running rendition
        try
        {
            this.renditionNode = transactionHelper
                    .doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<NodeRef>() {
                        public NodeRef execute() throws Throwable
                        {
                            // Initially the node that provides the content
                            // should not have the rn:renditioned aspect on it.
                            assertFalse("Source node has unexpected renditioned aspect.", nodeService.hasAspect(
                                    nodeWithDocContent, RenditionModel.ASPECT_RENDITIONED));

                            RenditionDefinition action = makeReformatAction(
                                    null, TEST_LONG_RUNNING_MIME_TYPE);

                            // Cancellation is only possible with tracking enabled
                            action.setTrackStatus(true);

                            // Render the content and put the result underneath the content node
                            ChildAssociationRef renditionAssoc = renditionService.render(nodeWithDocContent, action);

                            // Now the source content node should have the renditioned aspect
                            assertTrue("Source node is missing renditioned aspect.", nodeService.hasAspect(
                                    nodeWithDocContent, RenditionModel.ASPECT_RENDITIONED));
                            return renditionAssoc.getChildRef();
                        }
                    }, false, true);
        }
        catch (RenditionCancelledException e)
        {
            // expected cancellation exception
        }

        // Give a moment for roll back of rendition and commit of node modification to occur
        Thread.sleep(3000);

        if (joinNodeModifyingThread)
        {
            nodeModifyingThread.join();
        }

        // Note that the node modification is retried on failure by RetryingTransactionHelper
        // and will always succeed after the rendition is complete, but due to the
        // sleep in AbstractNodeModifyingRunnable isModificationUnblocked will still
        // be false if there was a failure
        if (nodeModifyingRunnable.getModificationException() != null)
        {
            throw new RuntimeException(
                    "Modification of node during long running rendition failed",
                    nodeModifyingRunnable.getModificationException());
        }
    }

    /**
     * Tests checkout during a long running transformation rendition.
     * 
     * @throws Exception
     */
    @Test
    public void testRenderPdfDocumentLongRunningCheckout() throws Exception
    {
        try
        {
            class CheckoutRunnable extends AbstractNodeModifyingRunnable
            {
                public CheckoutRunnable(NodeRef nodeRef)
                {
                    super(nodeRef);
                }

                protected void modifyNode(NodeRef nodeRef)
                {
                    AuthenticationUtil.setRunAsUserSystem();
                    CheckOutCheckInService checkOutCheckInService = (CheckOutCheckInService) applicationContext.getBean("checkOutCheckInService");
                    NodeRef wc = checkOutCheckInService.checkout(nodeRef);
                    assertNotNull(wc);
                }
            }
            ;
            renderPdfDocumentLongRunningTest(new CheckoutRunnable(nodeWithDocContent), true);
        }
        finally
        {
            transactionHelper.doInTransaction(() -> {
                AuthenticationUtil.setRunAsUserSystem();
                CheckOutCheckInService checkOutCheckInService = (CheckOutCheckInService) applicationContext.getBean("CheckOutCheckInService");
                NodeRef wc = checkOutCheckInService.getWorkingCopy(nodeWithDocContent);
                assertNotNull(wc);
                checkOutCheckInService.cancelCheckout(wc);
                return null;
            }, false, true);
        }
    }

    /**
     * Tests locking during a long running transformation rendition.
     * 
     * @throws Exception
     */
    @Test
    public void testRenderPdfDocumentLongRunningLock() throws Exception
    {
        class LockRunnable extends AbstractNodeModifyingRunnable
        {
            public LockRunnable(NodeRef nodeRef)
            {
                super(nodeRef);
            }

            protected void modifyNode(NodeRef nodeRef)
            {
                AuthenticationUtil.setRunAsUserSystem();
                LockService lockService = (LockService) applicationContext.getBean("lockService");
                lockService.lock(nodeRef, LockType.WRITE_LOCK);
            }
        }
        ;
        renderPdfDocumentLongRunningTest(new LockRunnable(nodeWithDocContent));
    }

    @Test
    public void testCompositeReformatAndResizeRendition() throws Exception
    {
        final QName renditionName = QName.createQName(NamespaceService.RENDITION_MODEL_1_0_URI, "composite");
        final int newX = 20;
        final int newY = 30;

        renditionNode = transactionHelper.doInTransaction(new RetryingTransactionHelper.//
                RetryingTransactionCallback<NodeRef>() {
            public NodeRef execute() throws Throwable
            {
                CompositeRenditionDefinition compositeDefinition = makeCompositeReformatAndResizeDefinition(
                        renditionName, newX, newY);
                ChildAssociationRef renditionAssoc = renditionService.render(nodeWithDocContent,
                        compositeDefinition);
                validateRenditionAssociation(renditionAssoc, renditionName);
                return renditionAssoc.getChildRef();
            }

        });

        transactionHelper.doInTransaction(new RetryingTransactionHelper.//
                RetryingTransactionCallback<Void>() {
            public Void execute() throws Throwable
            {
                List<ChildAssociationRef> renditions = renditionService.getRenditions(nodeWithDocContent);
                assertEquals("There should only be one rendition", 1, renditions.size());

                ChildAssociationRef renditionAssoc = renditions.get(0);
                assertEquals("The association name should match the composite rendition name",
                        renditionName, renditionAssoc.getQName());

                NodeRef rendition = renditionAssoc.getChildRef();
                ContentReader reader = contentService.getReader(rendition, ContentModel.PROP_CONTENT);
                assertEquals("The mimetype is wrong", MimetypeMap.MIMETYPE_IMAGE_JPEG, reader.getMimetype());

                assertNotNull("Reader to rendered image was null", reader);
                BufferedImage img = ImageIO.read(reader.getContentInputStream());

                assertEquals("Rendered image had wrong height", newY, img.getHeight());
                assertEquals("Rendered image had wrong width", newX, img.getWidth());
                return null;
            }
        });

    }

    /**
     * This test method used the RenditionService to render a test document (of type PDF) into a different format (of type plain_text) and place the rendition under the specified folder.
     */
    @Test
    public void testRenderDocumentInAnotherFormatUnderSpecifiedFolder() throws Exception
    {
        transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>() {
            public Void execute() throws Throwable
            {
                // Initially the node that provides the content should not have
                // the rn:renditioned aspect on it.
                assertFalse("Source node has unexpected renditioned aspect.", nodeService.hasAspect(nodeWithDocContent,
                        RenditionModel.ASPECT_RENDITIONED));

                validateRenderingActionDefinition(ReformatRenderingEngine.NAME);

                // Create the rendering action.
                RenditionDefinition definition = makeReformatAction(ContentModel.TYPE_CONTENT,
                        MimetypeMap.MIMETYPE_TEXT_PLAIN);
                Serializable targetFolderName = nodeService.getProperty(testTargetFolder, ContentModel.PROP_NAME);
                String path = "${companyHome.name}/" + targetFolderName + "/test.txt";
                definition.setParameterValue(RenditionService.PARAM_DESTINATION_PATH_TEMPLATE, path);

                // Perform the action with an explicit destination folder
                ChildAssociationRef renditionAssoc = renditionService.render(nodeWithDocContent, definition);
                NodeRef rendition = renditionAssoc.getChildRef();
                // A secondary parent
                assertEquals("The parent node was not correct", nodeWithDocContent, renditionAssoc.getParentRef());

                validateRenditionAssociation(renditionAssoc, REFORMAT_RENDER_DEFN_NAME);

                // The rendition node should have 2 parents: the containing
                // folder and the source node
                List<ChildAssociationRef> parentAssocs = nodeService.getParentAssocs(rendition);
                assertEquals("Wrong value for rendition node parent count.", 2, parentAssocs.size());

                // Check the parent nodeRefs are correct
                List<NodeRef> parents = new ArrayList<NodeRef>(2);
                parents.add(parentAssocs.get(0).getParentRef());
                parents.add(parentAssocs.get(1).getParentRef());
                assertTrue("Missing containing folder as parent", parents.contains(testTargetFolder));
                assertTrue("Missing source node as parent", parents.contains(nodeWithDocContent));

                // Now the source content node should have the rn:renditioned
                // aspect
                assertTrue("Source node is missing renditioned aspect.", nodeService.hasAspect(nodeWithDocContent,
                        RenditionModel.ASPECT_RENDITIONED));

                return null;
            }
        });
    }

    /**
     * This test method used the RenditionService to render a test image (of type PNG) as a cropped image of the same type.
     */
    @SuppressWarnings("unused")
    @Test
    public void testRenderCropImage() throws Exception
    {
        final int originalImageWidth = 512;
        final int originalImageHeight = 512;

        // Create a rendition of an existing image with specified absolute x and
        // y scale.
        final int imageNewXSize = 36;
        final int imageNewYSize = 47;
        final Map<String, Serializable> parameterValues = new HashMap<String, Serializable>();
        parameterValues.put(CropSourceOptionsSerializer.PARAM_CROP_WIDTH, imageNewXSize);
        parameterValues.put(CropSourceOptionsSerializer.PARAM_CROP_HEIGHT, imageNewYSize);

        ImageTransformationOptions imageTransformationOptions = new ImageTransformationOptions();
        final NodeRef newRenditionNode = performImageRendition(parameterValues, nodeWithImageContent);

        // Assert that the rendition is of the correct size and has reasonable
        // content.
        transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>() {
            public Void execute() throws Throwable
            {

                // The rescaled image rendition is a child of the original test
                // node.
                List<ChildAssociationRef> children = nodeService.getChildAssocs(nodeWithImageContent,
                        new RegexQNamePattern(getLongNameWithEscapedBraces(RenditionModel.ASSOC_RENDITION)),
                        new RegexQNamePattern(getLongNameWithEscapedBraces(RESCALE_RENDER_DEFN_NAME)));

                // There should only be one child of the image node: the
                // rendition we've just created.
                assertEquals("Unexpected number of children", 1, children.size());

                NodeRef newImageRendition = children.get(0).getChildRef();
                assertEquals(newRenditionNode, newImageRendition);

                ContentReader reader = contentService.getReader(newImageRendition, ContentModel.PROP_CONTENT);
                assertNotNull("Reader to rendered image was null", reader);
                BufferedImage img = ImageIO.read(reader.getContentInputStream());

                assertEquals("Rendered image had wrong height", imageNewYSize, img.getHeight());
                assertEquals("Rendered image had wrong width", imageNewXSize, img.getWidth());

                ContentReader srcReader = contentService.getReader(nodeWithImageContent, ContentModel.PROP_CONTENT);
                BufferedImage srcImg = ImageIO.read(srcReader.getContentInputStream());

                // The upper left pixel of the image should be pure black.
                int rgbAtTopLeft = img.getRGB(1, 1);
                int expRgbAtTopLeft = img.getRGB(1, 1);
                assertEquals("Incorrect image content.", expRgbAtTopLeft, rgbAtTopLeft);

                // The lower right pixel of the image should be pure white
                int rightIndex = img.getWidth() - 1;
                int bottomIndex = img.getHeight() - 1;
                int rgbAtBottomRight = img.getRGB(rightIndex, bottomIndex);
                int expRgbAtBottomRight = srcImg.getRGB(rightIndex, bottomIndex);
                assertEquals("Incorrect image content.", expRgbAtBottomRight, rgbAtBottomRight);

                return null;
            }
        });

        // Create a rendition of the same image, this time cropping by 50/25%
        parameterValues.clear();
        parameterValues.put(CropSourceOptionsSerializer.PARAM_CROP_WIDTH, 50); // 256 picels
        parameterValues.put(CropSourceOptionsSerializer.PARAM_CROP_HEIGHT, 25); // 128 pixels
        parameterValues.put(CropSourceOptionsSerializer.PARAM_IS_PERCENT_CROP, true);

        final NodeRef secondRenditionNode = performImageRendition(parameterValues, nodeWithImageContent);

        // Assert that the rendition is of the correct size and has reasonable
        // content.
        transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>() {
            public Void execute() throws Throwable
            {
                // The rescaled image rendition is a child of the original test
                // node.
                List<ChildAssociationRef> children = nodeService.getChildAssocs(nodeWithImageContent,
                        new RegexQNamePattern(getLongNameWithEscapedBraces(RenditionModel.ASSOC_RENDITION)),
                        new RegexQNamePattern(getLongNameWithEscapedBraces(RESCALE_RENDER_DEFN_NAME)));

                // There should only be one child of the image node: the
                // rendition we've just created.
                assertEquals("Unexpected number of children", 1, children.size());

                NodeRef newImageRendition = children.get(0).getChildRef();
                assertEquals(secondRenditionNode, newImageRendition);

                ContentReader srcReader = contentService.getReader(nodeWithImageContent, ContentModel.PROP_CONTENT);
                BufferedImage srcImg = ImageIO.read(srcReader.getContentInputStream());

                ContentReader reader = contentService.getReader(newImageRendition, ContentModel.PROP_CONTENT);
                assertNotNull("Reader to rendered image was null", reader);
                BufferedImage img = ImageIO.read(reader.getContentInputStream());

                assertEquals("Rendered image had wrong height", 128, img.getHeight());
                assertEquals("Rendered image had wrong width", 256, img.getWidth());

                // The upper left pixel of the image should be pure black.
                int rgbAtTopLeft = img.getRGB(1, 1);
                int expRgbAtTopLeft = srcImg.getRGB(1, 1);
                assertEquals("Incorrect image content.", expRgbAtTopLeft, rgbAtTopLeft);

                // The lower right pixel of the image should be pure white
                int widthIndex = img.getWidth() - 1;
                int heightIndex = img.getHeight() - 1;
                int rgbAtBottomRight = img.getRGB(widthIndex, heightIndex);
                int expRgbAtBottomRight = srcImg.getRGB(widthIndex, heightIndex);
                assertEquals("Incorrect image content.", expRgbAtBottomRight, rgbAtBottomRight);

                return null;
            }
        });
    }

    /**
     * This test method used the RenditionService to render a test image (of type PNG) as a rescaled image of the same type.
     */
    @Test
    public void testRenderRescaledImage() throws Exception
    {
        final int originalImageWidth = 512;
        final int originalImageHeight = 512;

        // Create a rendition of an existing image with specified absolute x and
        // y scale.
        final Integer imageNewXSize = Integer.valueOf(36);
        final Integer imageNewYSize = Integer.valueOf(48);
        final Map<String, Serializable> parameterValues = new HashMap<String, Serializable>();
        parameterValues.put(ImageRenderingEngine.PARAM_RESIZE_WIDTH, imageNewXSize);
        parameterValues.put(ImageRenderingEngine.PARAM_RESIZE_HEIGHT, imageNewYSize);

        final NodeRef newRenditionNode = performImageRendition(parameterValues, nodeWithImageContent);

        // Assert that the rendition is of the correct size and has reasonable
        // content.
        transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>() {
            public Void execute() throws Throwable
            {
                // The rescaled image rendition is a child of the original test
                // node.
                List<ChildAssociationRef> children = nodeService.getChildAssocs(nodeWithImageContent,
                        new RegexQNamePattern(getLongNameWithEscapedBraces(RenditionModel.ASSOC_RENDITION)),
                        new RegexQNamePattern(getLongNameWithEscapedBraces(RESCALE_RENDER_DEFN_NAME)));

                // There should only be one child of the image node: the
                // rendition we've just created.
                assertEquals("Unexpected number of children", 1, children.size());

                NodeRef newImageRendition = children.get(0).getChildRef();
                assertEquals(newRenditionNode, newImageRendition);

                ContentReader reader = contentService.getReader(newImageRendition, ContentModel.PROP_CONTENT);
                assertNotNull("Reader to rendered image was null", reader);
                BufferedImage img = ImageIO.read(reader.getContentInputStream());

                assertEquals("Rendered image had wrong height", imageNewYSize, Integer.valueOf(img.getHeight()));
                assertEquals("Rendered image had wrong width", imageNewXSize, Integer.valueOf(img.getWidth()));

                // The upper left pixel of the image should be pure black.
                int rgbAtTopLeft = img.getRGB(1, 1);
                assertTrue("Incorrect image content.", Integer.toHexString(rgbAtTopLeft).endsWith(BLACK));

                // The lower right pixel of the image should be pure white
                int rgbAtBottomRight = img.getRGB(img.getWidth() - 1, img.getHeight() - 1);
                assertTrue("Incorrect image content.", Integer.toHexString(rgbAtBottomRight).endsWith(WHITE));

                return null;
            }
        });

        // Create a rendition of the same image, this time rescaling by 200%
        parameterValues.clear();
        parameterValues.put(ImageRenderingEngine.PARAM_RESIZE_WIDTH, 200);
        parameterValues.put(ImageRenderingEngine.PARAM_RESIZE_HEIGHT, 200);
        parameterValues.put(ImageRenderingEngine.PARAM_IS_PERCENT_RESIZE, true);

        final NodeRef secondRenditionNode = performImageRendition(parameterValues, nodeWithImageContent);

        // Assert that the rendition is of the correct size and has reasonable
        // content.
        transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>() {
            public Void execute() throws Throwable
            {
                // The rescaled image rendition is a child of the original test
                // node.
                List<ChildAssociationRef> children = nodeService.getChildAssocs(nodeWithImageContent,
                        new RegexQNamePattern(getLongNameWithEscapedBraces(RenditionModel.ASSOC_RENDITION)),
                        new RegexQNamePattern(getLongNameWithEscapedBraces(RESCALE_RENDER_DEFN_NAME)));

                // There should only be one child of the image node: the
                // rendition we've just created.
                assertEquals("Unexpected number of children", 1, children.size());

                NodeRef newImageRendition = children.get(0).getChildRef();
                assertEquals(secondRenditionNode, newImageRendition);

                ContentReader reader = contentService.getReader(newImageRendition, ContentModel.PROP_CONTENT);
                assertNotNull("Reader to rendered image was null", reader);
                BufferedImage img = ImageIO.read(reader.getContentInputStream());

                assertEquals("Rendered image had wrong height", originalImageWidth * 2, img.getHeight());
                assertEquals("Rendered image had wrong width", originalImageHeight * 2, img.getWidth());

                // The upper left pixel of the image should be pure black.
                int rgbAtTopLeft = img.getRGB(1, 1);
                assertTrue("Incorrect image content.", Integer.toHexString(rgbAtTopLeft).endsWith(BLACK));

                // The lower right pixel of the image should be pure white
                int rgbAtBottomRight = img.getRGB(img.getWidth() - 1, img.getHeight() - 1);
                assertTrue("Incorrect image content.", Integer.toHexString(rgbAtBottomRight).endsWith(WHITE));

                return null;
            }
        });
    }

    /**
     * Tests that the ReformatActionExecutor can be used to render images into different formats.
     * 
     * @throws Exception
     */
    @Test
    public void testReformatImage() throws Exception
    {
        this.renditionNode = transactionHelper
                .doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<NodeRef>() {
                    public NodeRef execute() throws Throwable
                    {
                        // Initially the node that provides the content
                        // should not have the rn:renditioned aspect on it.
                        assertFalse("Source node has unexpected renditioned aspect.", nodeService.hasAspect(
                                nodeWithImageContent, RenditionModel.ASPECT_RENDITIONED));

                        RenditionDefinition action = makeReformatAction(null, MimetypeMap.MIMETYPE_TEXT_PLAIN);

                        // Set output Mimetype to JPEG.
                        action.setParameterValue(AbstractRenderingEngine.PARAM_MIME_TYPE,
                                MimetypeMap.MIMETYPE_IMAGE_JPEG);

                        ChildAssociationRef renditionAssoc = renditionService.render(nodeWithImageContent, action);
                        return renditionAssoc.getChildRef();
                    }
                });
        transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>() {
            public Void execute() throws Throwable
            {
                ContentReader reader = contentService.getReader(renditionNode, ContentModel.PROP_CONTENT);
                assertNotNull("Reader to rendered image was null", reader);
                assertEquals(MimetypeMap.MIMETYPE_IMAGE_JPEG, reader.getMimetype());

                BufferedImage img = ImageIO.read(reader.getContentInputStream());

                assertEquals("Rendered image had wrong height", 512, img.getHeight());
                assertEquals("Rendered image had wrong width", 512, img.getWidth());

                // The upper left pixel of the image should be pure black.
                int rgbAtTopLeft = img.getRGB(1, 1);
                assertTrue("Incorrect image content.", Integer.toHexString(rgbAtTopLeft).endsWith(BLACK));

                // The lower right pixel of the image should be pure white
                int rgbAtBottomRight = img.getRGB(img.getWidth() - 1, img.getHeight() - 1);
                assertTrue("Incorrect image content.", Integer.toHexString(rgbAtBottomRight).endsWith(WHITE));

                return null;
            }
        });
    }

    /**
     * Tests that source nodes with a suitable marker aspect on them are not renditioned.
     * 
     * @since 4.0.1
     */
    @Test
    public void testSuitablyMarkedNodesDoNotGetRenditioned() throws Exception
    {
        this.renditionNode = transactionHelper
                .doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<NodeRef>() {
                    public NodeRef execute() throws Throwable
                    {
                        // Initially the node that provides the content
                        // should not have the rn:renditioned aspect on it.
                        assertFalse("Source node has unexpected renditioned aspect.", nodeService.hasAspect(
                                nodeWithImageContent, RenditionModel.ASPECT_RENDITIONED));

                        // Add the marker aspect to prevent rendition
                        nodeService.addAspect(nodeWithImageContent, RenditionModel.ASPECT_PREVENT_RENDITIONS, null);

                        RenditionDefinition action = makeReformatAction(null, MimetypeMap.MIMETYPE_TEXT_PLAIN);
                        action.setParameterValue(AbstractRenderingEngine.PARAM_MIME_TYPE, MimetypeMap.MIMETYPE_IMAGE_JPEG);

                        ChildAssociationRef renditionAssoc = null;
                        boolean expectedExceptionThrown = false;
                        try
                        {
                            renditionAssoc = renditionService.render(nodeWithImageContent, action);
                        }
                        catch (RenditionServiceException expected)
                        {
                            expectedExceptionThrown = true;
                        }
                        assertTrue("Expected exception was not thrown.", expectedExceptionThrown);

                        return renditionAssoc == null ? null : renditionAssoc.getChildRef();
                    }
                });
    }

    @Test
    public void testSuccessfulAsynchronousRendition() throws Exception
    {
        // There are two relevant threads here: the JUnit test thread and the background
        // asynchronousActionExecution thread. It is this second thread that will do
        // the rendering work and I want to make sure that any failures on that thread
        // are returned to the JUnit thread in order to fail the test.
        // I also need to ensure that the asynchronous rendering is complete before
        // asserting anything about the results.
        // The countdown latch below is the mechanism by which the JUnit test code
        // waits for the completion of the other thread.
        final CountDownLatch latch = new CountDownLatch(1);
        final AsyncResultsHolder results = new AsyncResultsHolder();
        final RenderCallback callback = new RenderCallback() {
            public void handleFailedRendition(Throwable t)
            {
                results.setMessage("Rendition failed unexpectedly.");
                latch.countDown();
            }

            public void handleSuccessfulRendition(
                    ChildAssociationRef primaryParentOfNewRendition)
            {
                results.setAssoc(primaryParentOfNewRendition);
                latch.countDown();
            }
        };

        // We're performing this on a valid piece of content.
        // We expect this to succeed.
        performAsyncRendition(nodeWithImageContent, callback, latch, results);

        assertNotNull("ChildAssociationRef was null.", results.getAssoc());
        // We'll simply assert that the association has the correct parent.
        assertEquals(nodeWithImageContent, results.getAssoc().getParentRef());
        assertNull(results.getThrowable());
    }

    /**
     * 
     * @throws Exception
     * @see {@link #testSuccessfulAsynchronousRendition()}
     */
    @Test
    public void testFailedAsynchronousRendition() throws Exception
    {
        // see comment in method above for explanation of the countdown latch.
        final CountDownLatch latch = new CountDownLatch(1);
        final AsyncResultsHolder results = new AsyncResultsHolder();
        final RenderCallback callback = new RenderCallback() {
            public void handleFailedRendition(Throwable t)
            {
                results.setThrowable(t);
                latch.countDown();
            }

            public void handleSuccessfulRendition(
                    ChildAssociationRef primaryParentOfNewRendition)
            {
                results.setMessage("Rendition succeeded unexpectedly.");
                latch.countDown();
            }
        };

        // We're performing the render on an invalid node. We expect this to fail.
        performAsyncRendition(testTargetFolder, callback, latch, results);

        assertNull(results.getAssoc());
        assertEquals("Expected a UnimportantTransformException", UnimportantTransformException.class, results.getThrowable().getClass());
    }

    /**
     * This method performs an asynchronous rendition and calls back the result to the provided callback object. It uses the provided latch to coordinate the JUnit and the asynchronous action thread and uses the provided MutableString to send back any error messages for JUnit reporting.
     * <P/>
     * This method blocks until the action service thread either completes its work or times out.
     * 
     * @param callback
     * @param latch
     * @param failureMessage
     * @param nodeToRender
     * @throws InterruptedException
     */
    private void performAsyncRendition(final NodeRef nodeToRender, final RenderCallback callback, CountDownLatch latch,
            final AsyncResultsHolder failureMessage) throws InterruptedException
    {
        transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>() {
            public Void execute() throws Throwable
            {
                RenditionDefinition action = makeReformatAction(null, MimetypeMap.MIMETYPE_TEXT_PLAIN);

                action.setParameterValue(AbstractRenderingEngine.PARAM_MIME_TYPE, MimetypeMap.MIMETYPE_IMAGE_JPEG);

                renditionService.render(nodeToRender, action, callback);
                return null;
            }
        }, false, true);

        // Now wait for the actionService thread to complete the rendering.
        // We'll arbitrarily timeout after 30 seconds, which should be plenty for the
        // action to execute.
        boolean endedNormally = latch.await(30, TimeUnit.SECONDS);

        String failedString = failureMessage.getValue();
        if (failedString != null)
        {
            fail(failedString);
        }
        if (endedNormally == false)
        {
            fail("ActionService thread took too long to perform rendering.");
        }
    }

    /**
     * A simple struct class to allow the return of failure messages, exception objects and ChildAssociationRef results from asynchronous calls.
     */
    private static class AsyncResultsHolder
    {
        private String message;
        private ChildAssociationRef assoc;
        private Throwable throwable;

        public synchronized String getValue()
        {
            return message;
        }

        public synchronized void setMessage(String message)
        {
            this.message = message;
        }

        public synchronized ChildAssociationRef getAssoc()
        {
            return assoc;
        }

        public synchronized void setAssoc(ChildAssociationRef assoc)
        {
            this.assoc = assoc;
        }

        public synchronized Throwable getThrowable()
        {
            return throwable;
        }

        public synchronized void setThrowable(Throwable throwable)
        {
            this.throwable = throwable;
        }
    }

    @Test
    public void testGetRenditionsForNode() throws Exception
    {
        this.renditionNode = transactionHelper
                .doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<NodeRef>() {
                    public NodeRef execute() throws Throwable
                    {
                        // Initially the node that provides the content
                        // should have no renditions
                        assertTrue("Test node should have no renditions initially", renditionService.getRenditions(
                                nodeWithImageContent).isEmpty());

                        // Create 4 arbitrary rendition definitions
                        QName rendName1 = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "definition1"
                                + System.currentTimeMillis());
                        RenditionDefinition action1 = renditionService.createRenditionDefinition(rendName1,
                                ReformatRenderingEngine.NAME);
                        action1.setParameterValue(AbstractRenderingEngine.PARAM_MIME_TYPE,
                                MimetypeMap.MIMETYPE_IMAGE_GIF);

                        QName rendName2 = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "definition2"
                                + System.currentTimeMillis());
                        RenditionDefinition action2 = renditionService.createRenditionDefinition(rendName2,
                                ReformatRenderingEngine.NAME);
                        action2.setParameterValue(AbstractRenderingEngine.PARAM_MIME_TYPE,
                                MimetypeMap.MIMETYPE_IMAGE_JPEG);

                        QName rendName3 = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "definition3"
                                + System.currentTimeMillis());
                        RenditionDefinition action3 = renditionService.createRenditionDefinition(rendName3,
                                ImageRenderingEngine.NAME);
                        action3.setParameterValue(ImageRenderingEngine.PARAM_RESIZE_WIDTH, 64);
                        action3.setParameterValue(ImageRenderingEngine.PARAM_RESIZE_HEIGHT, 64);
                        action3.setParameterValue(AbstractRenderingEngine.PARAM_MIME_TYPE,
                                MimetypeMap.MIMETYPE_IMAGE_PNG);

                        // The 4th intentionally reuses the rendition name
                        // from the third
                        QName rendName4 = rendName3;
                        RenditionDefinition action4 = renditionService.createRenditionDefinition(rendName4,
                                ImageRenderingEngine.NAME);
                        action4.setParameterValue(ImageRenderingEngine.PARAM_RESIZE_WIDTH, 128);
                        action4.setParameterValue(ImageRenderingEngine.PARAM_RESIZE_HEIGHT, 128);
                        action4.setParameterValue(AbstractRenderingEngine.PARAM_MIME_TYPE,
                                MimetypeMap.MIMETYPE_IMAGE_PNG);

                        // Execute the 4 renditions.
                        ChildAssociationRef createdRendition1 = renditionService.render(nodeWithImageContent,
                                action1);
                        ChildAssociationRef createdRendition2 = renditionService.render(nodeWithImageContent,
                                action2);
                        ChildAssociationRef createdRendition3 = renditionService.render(nodeWithImageContent,
                                action3);
                        ChildAssociationRef createdRendition4 = renditionService.render(nodeWithImageContent,
                                action4);

                        // Now validate the getRenditions methods
                        List<ChildAssociationRef> allRenditions = renditionService
                                .getRenditions(nodeWithImageContent);
                        ChildAssociationRef retrievedRendition1 = renditionService.getRenditionByName(
                                nodeWithImageContent, rendName1);
                        ChildAssociationRef retrievedRendition2 = renditionService.getRenditionByName(
                                nodeWithImageContent, rendName2);
                        ChildAssociationRef retrievedRendition3 = renditionService.getRenditionByName(
                                nodeWithImageContent, rendName3);
                        ChildAssociationRef retrievedRendition4 = renditionService.getRenditionByName(
                                nodeWithImageContent, rendName4);

                        // allRenditions should contain only 3 renditions.
                        // The 4th should have replaced the 3rd.
                        assertEquals(3, allRenditions.size());
                        assertTrue(allRenditions.contains(createdRendition1));
                        assertTrue(allRenditions.contains(createdRendition2));
                        assertTrue(allRenditions.contains(createdRendition4));
                        for (ChildAssociationRef rendition : allRenditions)
                        {
                            assertNotSame(createdRendition3, rendition);
                        }

                        assertEquals(createdRendition1, retrievedRendition1);
                        assertEquals(createdRendition2, retrievedRendition2);
                        assertEquals(createdRendition4, retrievedRendition3);
                        assertEquals(createdRendition4, retrievedRendition4);

                        // CMIS-style filters. image/*
                        List<ChildAssociationRef> imageRenditions = renditionService.getRenditions(
                                nodeWithImageContent, "image");
                        assertEquals(3, imageRenditions.size());

                        List<ChildAssociationRef> imageSlashJRenditions = renditionService.getRenditions(
                                nodeWithImageContent, "image/j");
                        assertEquals(1, imageSlashJRenditions.size());

                        return null;
                    }
                });
    }

    protected NodeRef performImageRendition(final Map<String, Serializable> parameterValues, final NodeRef imageToRender)
    {
        final NodeRef newRenditionNode = transactionHelper
                .doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<NodeRef>() {
                    public NodeRef execute() throws Throwable
                    {
                        validateRenderingActionDefinition(ImageRenderingEngine.NAME);

                        // Create the rendering action.
                        RenditionDefinition action = renditionService.createRenditionDefinition(
                                RESCALE_RENDER_DEFN_NAME, ImageRenderingEngine.NAME);

                        // Set the parameters. Can't call
                        // action.setParameterValues as we don't want
                        // to obliterate existing parameters such as the
                        // name
                        for (String s : parameterValues.keySet())
                        {
                            action.setParameterValue(s, parameterValues.get(s));
                        }

                        ChildAssociationRef renditionAssoc = renditionService.render(imageToRender, action);

                        validateRenditionAssociation(renditionAssoc, RESCALE_RENDER_DEFN_NAME);

                        return renditionAssoc.getChildRef();
                    }
                });
        return newRenditionNode;
    }

    /**
     * Checks that the saveRenderingAction Method creates the proper node in the repository.
     */
    @Test
    public void testSaveRenderingAction() throws Exception
    {
        final List<RenditionDefinition> savedRenditionsToDelete = new ArrayList<RenditionDefinition>();
        try
        {
            // Check that if no node exists already then a new node is created.
            transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>() {
                public Void execute() throws Throwable
                {
                    assertTrue("The rendering action space has not been created in the repository!", //
                            nodeService.exists(RenditionDefinitionPersisterImpl.RENDERING_ACTION_ROOT_NODE_REF));
                    List<ChildAssociationRef> childAssocs = nodeService.getChildAssocs(//
                            RenditionDefinitionPersisterImpl.RENDERING_ACTION_ROOT_NODE_REF, //
                            ContentModel.ASSOC_CONTAINS, REFORMAT_RENDER_DEFN_NAME);
                    assertTrue("There should be no persisted rendering actions of name: " + REFORMAT_RENDER_DEFN_NAME
                            + " at the start of this test!", //
                            childAssocs.isEmpty());
                    RenditionDefinition action = makeReformatAction(null, MimetypeMap.MIMETYPE_TEXT_PLAIN);
                    savedRenditionsToDelete.add(action);

                    renditionService.saveRenditionDefinition(action);
                    List<ChildAssociationRef> results = nodeService.getChildAssocs(//
                            RenditionDefinitionPersisterImpl.RENDERING_ACTION_ROOT_NODE_REF, //
                            ContentModel.ASSOC_CONTAINS, REFORMAT_RENDER_DEFN_NAME);
                    assertEquals(
                            "There should be one persisted rendering action of name: " + REFORMAT_RENDER_DEFN_NAME, //
                            1, results.size());
                    return null;
                }
            });

            // Check that if a node already exists then that node is updated.
            transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>() {
                public Void execute() throws Throwable
                {
                    List<ChildAssociationRef> childAssocs = nodeService.getChildAssocs(//
                            RenditionDefinitionPersisterImpl.RENDERING_ACTION_ROOT_NODE_REF, //
                            ContentModel.ASSOC_CONTAINS, REFORMAT_RENDER_DEFN_NAME);
                    assertEquals(
                            "There should be one persisted rendering action of name: " + REFORMAT_RENDER_DEFN_NAME, //
                            1, childAssocs.size());
                    NodeRef actionNode = childAssocs.get(0).getChildRef();

                    RenditionDefinition action = makeReformatAction(null, MimetypeMap.MIMETYPE_TEXT_PLAIN);
                    savedRenditionsToDelete.add(action);

                    renditionService.saveRenditionDefinition(action);
                    List<ChildAssociationRef> results = nodeService.getChildAssocs(//
                            RenditionDefinitionPersisterImpl.RENDERING_ACTION_ROOT_NODE_REF, //
                            ContentModel.ASSOC_CONTAINS, REFORMAT_RENDER_DEFN_NAME);
                    assertEquals(
                            "There should be one persisted rendering action of name: " + REFORMAT_RENDER_DEFN_NAME, //
                            1, results.size());
                    assertEquals("The node in which the action is stored should be the same.", //
                            actionNode, results.get(0).getChildRef());
                    return null;
                }
            });
        }
        finally
        {
            cleanUpPersistedActions(savedRenditionsToDelete);
        }
    }

    /**
     * This is not a real test method. It is used to create the ACP file that we have added to the RenditionService for importing at startup. This method should remain here as it will be needed if we need to change the contents of the ACP.
     * 
     * @throws Exception
     * @deprecated
     */
    public void off_test_CleanPersistedRenditionsAndCreateExportedACP() throws Exception
    {
        // Check that if no node exists already then a new node is created.
        transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>() {
            public Void execute() throws Throwable
            {
                // Delete all renditionDefinitions and the folder that holds
                // them.
                // We can't delete and recreate the
                // RENDERING_ACTION_ROOT_NODE_REF and recreate it
                // here because we need to keep its well-known nodeRef id.

                assertTrue("The rendering action space has not been created in the repository!", //
                        nodeService.exists(RenditionDefinitionPersisterImpl.RENDERING_ACTION_ROOT_NODE_REF));

                // Clean up all existing saved actions here so as to ensure a
                // clean export.
                // Also delete any old acp file
                for (ChildAssociationRef chRef : nodeService
                        .getChildAssocs(RenditionDefinitionPersisterImpl.RENDERING_ACTION_ROOT_NODE_REF))
                {
                    System.out.println("Deleting rendition Definition "
                            + nodeService.getProperty(chRef.getChildRef(), ContentModel.PROP_NAME));
                    nodeService.deleteNode(chRef.getChildRef());
                }

                // Create the rendition definitions.

                // 1. "medium"
                Map<String, Serializable> parameterValues = new HashMap<String, Serializable>();
                parameterValues.put(AbstractRenderingEngine.PARAM_MIME_TYPE, MimetypeMap.MIMETYPE_IMAGE_JPEG);
                parameterValues.put(ImageRenderingEngine.PARAM_RESIZE_WIDTH, 100);
                parameterValues.put(ImageRenderingEngine.PARAM_RESIZE_HEIGHT, 100);
                parameterValues.put(ImageRenderingEngine.PARAM_MAINTAIN_ASPECT_RATIO, true);
                parameterValues.put(ImageRenderingEngine.PARAM_RESIZE_TO_THUMBNAIL, true);
                parameterValues.put(AbstractRenderingEngine.PARAM_PLACEHOLDER_RESOURCE_PATH,
                        "alfresco/thumbnail/thumbnail_placeholder_medium.jpg");
                parameterValues.put(AbstractRenderingEngine.PARAM_RUN_AS, AuthenticationUtil.getSystemUserName());

                RenditionDefinition action = createAction(ImageRenderingEngine.NAME, "medium", parameterValues);
                renditionService.saveRenditionDefinition(action);

                // 2. "doclib"
                parameterValues.clear();
                parameterValues.put(AbstractRenderingEngine.PARAM_MIME_TYPE, MimetypeMap.MIMETYPE_IMAGE_PNG);
                parameterValues.put(ImageRenderingEngine.PARAM_RESIZE_WIDTH, 100);
                parameterValues.put(ImageRenderingEngine.PARAM_RESIZE_HEIGHT, 100);
                parameterValues.put(ImageRenderingEngine.PARAM_MAINTAIN_ASPECT_RATIO, true);
                parameterValues.put(ImageRenderingEngine.PARAM_RESIZE_TO_THUMBNAIL, true);
                parameterValues.put(ImageRenderingEngine.PARAM_ALLOW_ENLARGEMENT, false);
                parameterValues.put(AbstractRenderingEngine.PARAM_PLACEHOLDER_RESOURCE_PATH,
                        "alfresco/thumbnail/thumbnail_placeholder_doclib.png");
                parameterValues.put(AbstractRenderingEngine.PARAM_RUN_AS, AuthenticationUtil.getSystemUserName());

                action = createAction(ImageRenderingEngine.NAME, "doclib", parameterValues);
                renditionService.saveRenditionDefinition(action);

                // 3. "webpreview"
                parameterValues.clear();
                parameterValues.put(AbstractRenderingEngine.PARAM_MIME_TYPE, MimetypeMap.MIMETYPE_FLASH);
                parameterValues.put(ReformatRenderingEngine.PARAM_FLASH_VERSION, "9");
                parameterValues.put(AbstractRenderingEngine.PARAM_RUN_AS, AuthenticationUtil.getSystemUserName());
                // no placeholder

                action = createAction("reformat", "webpreview", parameterValues);
                renditionService.saveRenditionDefinition(action);

                // 4. "imgpreview"
                parameterValues.clear();
                parameterValues.put(AbstractRenderingEngine.PARAM_MIME_TYPE, MimetypeMap.MIMETYPE_IMAGE_JPEG);
                parameterValues.put(ImageRenderingEngine.PARAM_RESIZE_WIDTH, 960);
                parameterValues.put(ImageRenderingEngine.PARAM_RESIZE_HEIGHT, 960);
                parameterValues.put(ImageRenderingEngine.PARAM_MAINTAIN_ASPECT_RATIO, true);
                parameterValues.put(ImageRenderingEngine.PARAM_RESIZE_TO_THUMBNAIL, true);
                parameterValues.put(ImageRenderingEngine.PARAM_ALLOW_ENLARGEMENT, false);
                parameterValues.put(AbstractRenderingEngine.PARAM_PLACEHOLDER_RESOURCE_PATH,
                        "alfresco/thumbnail/thumbnail_placeholder_imgpreview.png");
                parameterValues.put(AbstractRenderingEngine.PARAM_RUN_AS, AuthenticationUtil.getSystemUserName());

                action = createAction(ImageRenderingEngine.NAME, "imgpreview", parameterValues);
                renditionService.saveRenditionDefinition(action);

                // 5. "avatar"
                parameterValues.clear();
                parameterValues.put(AbstractRenderingEngine.PARAM_MIME_TYPE, MimetypeMap.MIMETYPE_IMAGE_PNG);
                parameterValues.put(ImageRenderingEngine.PARAM_RESIZE_WIDTH, 64);
                parameterValues.put(ImageRenderingEngine.PARAM_RESIZE_HEIGHT, 64);
                parameterValues.put(ImageRenderingEngine.PARAM_MAINTAIN_ASPECT_RATIO, true);
                parameterValues.put(ImageRenderingEngine.PARAM_RESIZE_TO_THUMBNAIL, true);
                parameterValues.put(AbstractRenderingEngine.PARAM_PLACEHOLDER_RESOURCE_PATH,
                        "alfresco/thumbnail/thumbnail_placeholder_avatar.png");
                parameterValues.put(AbstractRenderingEngine.PARAM_RUN_AS, AuthenticationUtil.getSystemUserName());

                action = createAction(ImageRenderingEngine.NAME, "avatar", parameterValues);
                renditionService.saveRenditionDefinition(action);
                return null;
            }

            private RenditionDefinition createAction(String renderingEngineName, String renditionLocalName,
                    Map<String, Serializable> parameterValues)
            {
                QName renditionName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, renditionLocalName);
                RenditionDefinition action = renditionService.createRenditionDefinition(renditionName,
                        renderingEngineName);
                for (String paramKey : parameterValues.keySet())
                {
                    action.setParameterValue(paramKey, parameterValues.get(paramKey));
                }
                return action;
            }
        });

        transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>() {
            public Void execute() throws Throwable
            {
                Action exportAction = actionService.createAction("export");
                exportAction.setParameterValue(ExporterActionExecuter.PARAM_STORE,
                        StoreRef.STORE_REF_WORKSPACE_SPACESSTORE.toString());
                exportAction.setParameterValue(ExporterActionExecuter.PARAM_PACKAGE_NAME, "systemRenditionDefinitions");
                exportAction.setParameterValue(ExporterActionExecuter.PARAM_ENCODING, "UTF-8");
                exportAction.setParameterValue(ExporterActionExecuter.PARAM_DESTINATION_FOLDER,
                        RenditionDefinitionPersisterImpl.RENDERING_ACTION_ROOT_NODE_REF);
                exportAction.setParameterValue(ExporterActionExecuter.PARAM_INCLUDE_SELF, true);
                exportAction.setParameterValue(ExporterActionExecuter.PARAM_INCLUDE_CHILDREN, true);

                actionService.executeAction(exportAction, RenditionDefinitionPersisterImpl.RENDERING_ACTION_ROOT_NODE_REF);

                return null;
            }
        });

        transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>() {
            public Void execute() throws Throwable
            {
                List<ChildAssociationRef> children = nodeService
                        .getChildAssocs(RenditionDefinitionPersisterImpl.RENDERING_ACTION_ROOT_NODE_REF);
                for (ChildAssociationRef car : children)
                {
                    System.err.println(car.getChildRef() + "  "
                            + nodeService.getProperty(car.getChildRef(), ContentModel.PROP_NAME));
                }

                return null;
            }
        });
    }

    /**
     * This test method saves one RenderingAction to the repository and loads it back up asserting that it is equivalent to the original. It then saves some further RenderingActions, loads them back with a RenderingEngine filter and asserts that the results are correct.
     * 
     * @throws Exception
     */
    @Test
    public void testLoadRenderingAction() throws Exception
    {
        final RenditionDefinition reformatAction = makeReformatAction(null, MimetypeMap.MIMETYPE_TEXT_PLAIN);
        final RenditionDefinition rescaleAction = makeRescaleImageAction();

        final List<RenditionDefinition> savedRenditionDefinitions = new ArrayList<RenditionDefinition>();
        try
        {
            // First save one action.
            transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>() {
                public Void execute() throws Throwable
                {
                    savedRenditionDefinitions.add(reformatAction);
                    renditionService.saveRenditionDefinition(reformatAction);
                    return null;
                }
            });

            // Then load the action back up and check it matches the original.
            transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>() {
                public Void execute() throws Throwable
                {
                    QName renditionName = reformatAction.getRenditionName();
                    RenditionDefinition result = renditionService.loadRenditionDefinition(renditionName);
                    assertEquals(renditionName, result.getRenditionName());
                    assertEquals(reformatAction.getActionDefinitionName(), result.getActionDefinitionName());
                    assertEquals(reformatAction.getCompensatingAction(), result.getCompensatingAction());
                    assertEquals(reformatAction.getDescription(), result.getDescription());
                    assertEquals(reformatAction.getExecuteAsychronously(), result.getExecuteAsychronously());
                    assertEquals(reformatAction.getModifiedDate(), result.getModifiedDate());
                    assertEquals(reformatAction.getModifier(), result.getModifier());
                    assertEquals(reformatAction.getTitle(), result.getTitle());

                    return null;
                }
            });

            // Now save the second action.
            transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>() {
                public Void execute() throws Throwable
                {
                    savedRenditionDefinitions.add(rescaleAction);
                    renditionService.saveRenditionDefinition(rescaleAction);
                    return null;
                }
            });

            // Then load the actions back up and check they exist and are
            // filtered correctly.
            transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>() {
                public Void execute() throws Throwable
                {
                    // Retrieve persisted Rendering Definitions by name.
                    RenditionDefinition firstLoadedAction = renditionService.loadRenditionDefinition(reformatAction
                            .getRenditionName());
                    RenditionDefinition secondLoadedAction = renditionService.loadRenditionDefinition(rescaleAction
                            .getRenditionName());
                    assertNotNull("The reformat action was null.", firstLoadedAction);
                    assertNotNull("The rescale action was null.", secondLoadedAction);

                    /* We'll not retest the action metadata here as that is tested in this method above. Just the names to ensure they are distinct. */
                    assertEquals(reformatAction.getRenditionName(), firstLoadedAction.getRenditionName());
                    assertEquals(rescaleAction.getRenditionName(), secondLoadedAction.getRenditionName());

                    // Retrieve all rendering definitions
                    // List<RenditionDefinition> renderingDefinitions =
                    // renditionService.loadRenditionDefinitions();
                    // assertEquals("Wrong number of 'all actions'.", 2,
                    // renderingDefinitions.size());

                    // Retrieve all rendering definitions for a given Rendering
                    // Engine.
                    // List<RenditionDefinition> renderDefinitionsForReformat =
                    // renditionService
                    // .loadRenditionDefinitions("reformat");
                    // assertEquals("Wrong number of actions for engine name.",
                    // 1, renderDefinitionsForReformat.size());

                    return null;
                }
            });
        }
        finally
        {
            cleanUpPersistedActions(savedRenditionDefinitions);
        }
    }

    @Test
    public void testSaveAndLoadCompositeRenditionDefinition() throws Exception
    {
        final QName renditionName = QName.createQName(NamespaceService.RENDITION_MODEL_1_0_URI, "composite");
        final CompositeRenditionDefinition compositeDefinition = makeCompositeReformatAndResizeDefinition(
                renditionName, 20, 30);
        final List<RenditionDefinition> savedRenditionDefinitions = new ArrayList<RenditionDefinition>();

        try
        {
            // First save composite rendition definition.
            transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>() {
                public Void execute() throws Throwable
                {
                    renditionService.saveRenditionDefinition(compositeDefinition);
                    return null;
                }
            });

            // Then load the definition back up and check it matches the
            // original.
            transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>() {
                public Void execute() throws Throwable
                {
                    RenditionDefinition result = renditionService.loadRenditionDefinition(renditionName);

                    // Check basic rendition definition properties.
                    assertEquals(renditionName, result.getRenditionName());
                    assertEquals(compositeDefinition.getActionDefinitionName(), result.getActionDefinitionName());
                    assertEquals(compositeDefinition.getCompensatingAction(), result.getCompensatingAction());
                    assertEquals(compositeDefinition.getDescription(), result.getDescription());
                    assertEquals(compositeDefinition.getExecuteAsychronously(), result.getExecuteAsychronously());
                    assertEquals(compositeDefinition.getModifiedDate(), result.getModifiedDate());
                    assertEquals(compositeDefinition.getModifier(), result.getModifier());
                    assertEquals(compositeDefinition.getTitle(), result.getTitle());

                    // Check sub-actions.
                    if (result instanceof CompositeRenditionDefinition)
                    {
                        CompositeRenditionDefinition compositeResult = (CompositeRenditionDefinition) result;
                        savedRenditionDefinitions.add(compositeResult);

                        List<RenditionDefinition> subDefinitions = compositeResult.getActions();
                        assertEquals(2, subDefinitions.size());

                        // Check the first sub-definition is correct.
                        RenditionDefinition firstDef = subDefinitions.get(0);
                        assertEquals(ReformatRenderingEngine.NAME, firstDef.getActionDefinitionName());

                        // Check the second sub-definition is correct.
                        RenditionDefinition secondDef = subDefinitions.get(1);
                        assertEquals(ImageRenderingEngine.NAME, secondDef.getActionDefinitionName());
                    }
                    else
                        fail("The retrieved rendition should be a CompositeRenditionDefinition.");
                    return null;
                }
            });
        }
        finally
        {
            cleanUpPersistedActions(savedRenditionDefinitions);
        }

    }

    /**
     * This method deletes the specified saved action nodes.
     */
    private void cleanUpPersistedActions(final List<RenditionDefinition> savedRenditionDefinitions)
    {
        transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>() {
            public Void execute() throws Throwable
            {
                for (RenditionDefinition savedRenditionDefinition : savedRenditionDefinitions)
                {
                    NodeRef actionNodeRef = savedRenditionDefinition.getNodeRef();
                    if (actionNodeRef != null)
                    {
                        nodeService.deleteNode(actionNodeRef);
                    }
                }
                return null;
            }
        });
    }

    /**
     * This test method ensures that all the 'built-in' renditionDefinitions are available after startup and that their configuration is correct.
     * 
     * @throws Exception
     */
    @Test
    public void testBuiltinRenditionDefinitions() throws Exception
    {
        final RenditionDefinition mediumRenditionDef = loadAndValidateRenditionDefinition("medium");
        final RenditionDefinition doclibRenditionDef = loadAndValidateRenditionDefinition("doclib");
        final RenditionDefinition imgpreviewRenditionDef = loadAndValidateRenditionDefinition("imgpreview");
        final RenditionDefinition webpreviewRenditionDef = loadAndValidateRenditionDefinition("webpreview");
        final RenditionDefinition avatarRenditionDef = loadAndValidateRenditionDefinition("avatar");

        assertEquals(MimetypeMap.MIMETYPE_IMAGE_JPEG, mediumRenditionDef.getParameterValue(AbstractRenderingEngine.PARAM_MIME_TYPE));
        assertEquals(MimetypeMap.MIMETYPE_IMAGE_PNG, doclibRenditionDef.getParameterValue(AbstractRenderingEngine.PARAM_MIME_TYPE));
        assertEquals(MimetypeMap.MIMETYPE_IMAGE_JPEG, imgpreviewRenditionDef.getParameterValue(AbstractRenderingEngine.PARAM_MIME_TYPE));
        assertEquals(MimetypeMap.MIMETYPE_FLASH, webpreviewRenditionDef.getParameterValue(AbstractRenderingEngine.PARAM_MIME_TYPE));
        assertEquals(MimetypeMap.MIMETYPE_IMAGE_PNG, avatarRenditionDef.getParameterValue(AbstractRenderingEngine.PARAM_MIME_TYPE));
    }

    /**
     * This test checks that for a node with an existing rendition, that if you update its content with content that cannot be renditioned (thumbnailed), that existing rendition nodes for failed re-renditions are removed. See ALF-6730.
     * 
     * @since 3.4.2
     */
    @Test
    public void testRenderValidContentThenUpdateToInvalidContent() throws Exception
    {
        transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>() {
            public Void execute() throws Throwable
            {
                // Need to use one of the standard, persisted rendition definitions, as non-persisted rendition definitions
                // do not get automatcially updated.
                RenditionDefinition doclib = loadAndValidateRenditionDefinition("doclib");

                // We want the rendition to be asynchronous, but we don't care about called-back data.
                RenderCallback dummyCallback = new RenderCallback() {
                    @Override
                    public void handleFailedRendition(Throwable t)
                    {}

                    @Override
                    public void handleSuccessfulRendition(
                            ChildAssociationRef primaryParentOfNewRendition)
                    {}
                };
                renditionService.render(nodeWithDocContent, doclib, dummyCallback);
                return null;
            }
        }, false, true);

        // Now wait for the actionService thread to complete the rendering.
        Thread.sleep(5000);

        // The rendition should have succeeded.
        transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>() {
            public Void execute() throws Throwable
            {
                List<ChildAssociationRef> renditions = renditionService.getRenditions(nodeWithDocContent);
                assertNotNull("Renditions missing.", renditions);
                assertEquals("Wrong rendition count", 1, renditions.size());
                return null;
            }
        }, false, true);

        // Now update the content to a corrupt PDF file that cannot be rendered.
        transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>() {
            public Void execute() throws Throwable
            {
                ContentWriter writer = contentService.getWriter(nodeWithDocContent, ContentModel.PROP_CONTENT, true);
                writer.setMimetype(MimetypeMap.MIMETYPE_PDF);
                writer.setEncoding("UTF-8");

                InputStream corruptIn = applicationContext.getResource("classpath:/quick/quickCorrupt.pdf").getInputStream();
                writer.putContent(corruptIn);
                corruptIn.close();

                return null;
            }
        }, false, true);

        // Now wait for the actionService thread to complete the rendition removal.
        Thread.sleep(5000);

        transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>() {
            public Void execute() throws Throwable
            {
                List<ChildAssociationRef> renditions = renditionService.getRenditions(nodeWithDocContent);
                assertNotNull("Renditions missing.", renditions);
                assertEquals("Wrong rendition count", 0, renditions.size());
                return null;
            }
        }, false, true);
    }

    @Test
    public void testALF3733() throws Exception
    {
        // ALF-3733 was caused by ${cwd} evaluating to the empty string and a path "//sourceNodeName"
        // being passed to the FileFolderService for creation. This then splits the string using '/' as
        // a delimiter which leads to the attempted creation of nodes with the empty string as a name,
        // which is illegal.
        QName renditionDefName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "myDummyRendition");
        String renderingEngineName = "imageRenderingEngine";

        RenditionDefinition renditionDef = renditionService.createRenditionDefinition(renditionDefName, renderingEngineName);
        Map<String, Serializable> params = new HashMap<String, Serializable>();
        params.put(ImageRenderingEngine.PARAM_RESIZE_WIDTH, 99);
        params.put(ImageRenderingEngine.PARAM_RESIZE_HEIGHT, 99);
        params.put(RenditionService.PARAM_DESTINATION_PATH_TEMPLATE, "${cwd}/resized/${name}_Thumb.${extension}");
        renditionDef.addParameterValues(params);

        ChildAssociationRef rendition = renditionService.render(this.nodeWithImageContent, renditionDef);

        assertNotNull("rendition was null.", rendition);
        assertTrue(nodeService.hasAspect(rendition.getChildRef(), RenditionModel.ASPECT_VISIBLE_RENDITION));

        // The rendition should have 2 parents.
        List<ChildAssociationRef> allParents = nodeService.getParentAssocs(rendition.getChildRef());
        assertEquals(2, allParents.size());

        // The rendition should be created under a new folder called 'resized'
        NodeRef primaryParent = nodeService.getPrimaryParent(rendition.getChildRef()).getParentRef();
        assertEquals("resized", nodeService.getProperty(primaryParent, ContentModel.PROP_NAME));
        assertEquals(ContentModel.TYPE_FOLDER, nodeService.getType(primaryParent));

        // The rendition should have the source node as a non-primary parent.
        NodeRef nonPrimaryParent = null;
        for (ChildAssociationRef chAss : allParents)
        {
            if (!chAss.getParentRef().equals(primaryParent))
            {
                nonPrimaryParent = chAss.getParentRef();
            }
        }
        assertNotNull("Non-primary parent was not found.", nonPrimaryParent);
        assertEquals(nodeWithImageContent, nonPrimaryParent);
    }

    /**
     * If we're using path based renditions, it's allowed for two source nodes to end up rendering into folder with the same name as each other. One case is /images/world/logo.png -> /smaller/logo.png /images/europe/logo.png -> /smaller/logo.png Clearly this isn't a great situation to be in, since the two source nodes are fighting over the rendered node! However, people will sometimes end up doing this, so we need to ensure that the last rendered version wins, and the previous rendered version is fully gone. We mustn't end up with both source nodes thinking they have the same rendition node, because it'd only be one of theirs!
     */
    @Test
    public void testPathBasedRenditionOverwrite() throws Exception
    {

    }

    /**
     * Using a dummy rendition engine, perform a number of renditions, both single and composite, to check that the renditions always end up as they should do.
     */
    @Test
    public void testRenditionPlacements() throws Exception
    {
        QName plainQName = QName.createQName("Plain");
        RenditionDefinition rdPlain = renditionService.createRenditionDefinition(
                plainQName, DummyHelloWorldRenditionEngine.ENGINE_NAME);

        QName compositeQName = QName.createQName("Composite");
        CompositeRenditionDefinition rdComposite = renditionService.createCompositeRenditionDefinition(
                compositeQName);
        rdComposite.addAction(renditionService.createRenditionDefinition(
                QName.createQName("CompositePart1"), DummyHelloWorldRenditionEngine.ENGINE_NAME));
        rdComposite.addAction(renditionService.createRenditionDefinition(
                QName.createQName("CompositePart2"), DummyHelloWorldRenditionEngine.ENGINE_NAME));

        // ============================================== //
        // Anonymous Rendition, no existing one there //
        // ============================================== //

        assertNotNull(nodeWithDocContent);
        assertEquals(0, renditionService.getRenditions(nodeWithDocContent).size());
        assertEquals(0, nodeService.getChildAssocs(nodeWithDocContent).size());

        // Do a plain rendition, and check we acquired the one node
        renditionService.render(nodeWithDocContent, rdPlain);
        ChildAssociationRef renditionAssoc = renditionService.getRenditionByName(nodeWithDocContent, plainQName);
        assertNotNull(renditionAssoc);
        assertEquals(1, nodeService.getChildAssocs(nodeWithDocContent).size());
        assertEquals(plainQName, nodeService.getChildAssocs(nodeWithDocContent).get(0).getQName());

        // Tidy
        nodeService.deleteNode(
                renditionService.getRenditions(nodeWithDocContent).get(0).getChildRef());
        assertNotNull(nodeWithDocContent);
        assertEquals(0, renditionService.getRenditions(nodeWithDocContent).size());
        assertEquals(0, nodeService.getChildAssocs(nodeWithDocContent).size());

        // Now do a composite rendition
        // Should once again have one node, despite there having been intermediate
        // nodes created during the composite stage
        renditionService.render(nodeWithDocContent, rdComposite);
        assertEquals(1, renditionService.getRenditions(nodeWithDocContent).size());
        assertEquals(1, nodeService.getChildAssocs(nodeWithDocContent).size());
        renditionAssoc = renditionService.getRenditionByName(nodeWithDocContent, compositeQName);
        assertNotNull(renditionAssoc);
        assertEquals(compositeQName, nodeService.getChildAssocs(nodeWithDocContent).get(0).getQName());

        // Tidy
        nodeService.deleteNode(
                renditionService.getRenditions(nodeWithDocContent).get(0).getChildRef());

        // ================================================ //
        // Anonymous Rendition, existing one, same type //
        // ================================================ //

        // Create one of the right type for a plain rendition
        renditionService.render(nodeWithDocContent, rdPlain);
        assertEquals(1, renditionService.getRenditions(nodeWithDocContent).size());
        assertEquals(1, nodeService.getChildAssocs(nodeWithDocContent).size());
        renditionAssoc = renditionService.getRenditionByName(nodeWithDocContent, plainQName);
        assertNotNull(renditionAssoc);

        // Run again, shouldn't change, should re-use the node
        renditionNode = renditionAssoc.getChildRef();
        renditionService.render(nodeWithDocContent, rdPlain);
        assertEquals(1, renditionService.getRenditions(nodeWithDocContent).size());
        assertEquals(1, nodeService.getChildAssocs(nodeWithDocContent).size());
        renditionAssoc = renditionService.getRenditionByName(nodeWithDocContent, plainQName);
        assertNotNull(renditionAssoc);
        assertEquals(renditionNode, renditionAssoc.getChildRef());
        assertEquals(plainQName, nodeService.getChildAssocs(nodeWithDocContent).get(0).getQName());

        // Tidy, and re-create for composite
        nodeService.deleteNode(
                renditionService.getRenditions(nodeWithDocContent).get(0).getChildRef());
        renditionService.render(nodeWithDocContent, rdComposite);
        assertEquals(1, renditionService.getRenditions(nodeWithDocContent).size());
        assertEquals(1, nodeService.getChildAssocs(nodeWithDocContent).size());
        renditionAssoc = renditionService.getRenditionByName(nodeWithDocContent, compositeQName);
        assertNotNull(renditionAssoc);

        // Run again, shouldn't change, should re-use the node
        renditionNode = renditionAssoc.getChildRef();
        renditionService.render(nodeWithDocContent, rdComposite);
        assertEquals(1, renditionService.getRenditions(nodeWithDocContent).size());
        assertEquals(1, nodeService.getChildAssocs(nodeWithDocContent).size());
        renditionAssoc = renditionService.getRenditionByName(nodeWithDocContent, compositeQName);
        assertNotNull(renditionAssoc);
        assertEquals(renditionNode, renditionAssoc.getChildRef());
        assertEquals(compositeQName, nodeService.getChildAssocs(nodeWithDocContent).get(0).getQName());

        // Tidy
        nodeService.deleteNode(
                renditionService.getRenditions(nodeWithDocContent).get(0).getChildRef());

        // ================================================= //
        // Anonymous Rendition, existing one, wrong type //
        // Note - this situation is not possible! //
        // ================================================= //

        // ================================================= //
        // Switch to being path based //
        // ================================================= //

        String fileName = "HelloWorld.txt";
        String path = "/" +
                (String) nodeService.getProperty(repositoryHelper.getCompanyHome(), ContentModel.PROP_NAME) +
                "/" +
                (String) nodeService.getProperty(testTargetFolder, ContentModel.PROP_NAME) +
                "/" + fileName;

        rdPlain.setParameterValue(
                RenditionService.PARAM_DESTINATION_PATH_TEMPLATE, path);
        rdComposite.setParameterValue(
                RenditionService.PARAM_DESTINATION_PATH_TEMPLATE, path);
        QName expectedName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, fileName);

        // ================================================= //
        // Path based rendition, no existing one there //
        // ================================================= //

        assertNotNull(nodeWithDocContent);
        assertEquals(0, renditionService.getRenditions(nodeWithDocContent).size());
        assertEquals(0, nodeService.getChildAssocs(nodeWithDocContent).size());
        List<ChildAssociationRef> children = nodeService.getChildAssocs(testTargetFolder);
        assertEquals(0, children.size());

        // Do a plain rendition, and check we acquired the one node
        renditionService.render(nodeWithDocContent, rdPlain);
        assertEquals(1, renditionService.getRenditions(nodeWithDocContent).size());
        assertEquals(1, nodeService.getChildAssocs(nodeWithDocContent).size());
        renditionAssoc = renditionService.getRenditionByName(nodeWithDocContent, plainQName);
        assertNotNull(renditionAssoc);
        assertFalse(renditionAssoc.isPrimary());
        children = nodeService.getChildAssocs(testTargetFolder);
        assertEquals(1, children.size());
        ChildAssociationRef childAssoc = children.get(0);
        assertEquals(expectedName, childAssoc.getQName());
        assertTrue(childAssoc.isPrimary());

        nodeService.deleteNode(
                renditionService.getRenditions(nodeWithDocContent).get(0).getChildRef());

        assertNotNull(nodeWithDocContent);
        assertEquals(0, renditionService.getRenditions(nodeWithDocContent).size());
        // FIXME There is a bug in the NodeService whereby associations to children in the archive store
        // i.e. deleted children, are included in the results to the getChildAssocs call.
        // Therefore, pending a fix to that, we need to suppress this check and similar checks below.
        // assertEquals(0, nodeService.getChildAssocs(nodeWithDocContent).size());
        assertEquals(0, nodeService.getChildAssocs(testTargetFolder).size());

        // Now do a composite rendition
        // Should once again have one node, despite there having been intermediate
        // nodes created during the composite stage
        renditionService.render(nodeWithDocContent, rdComposite);
        assertEquals(1, renditionService.getRenditions(nodeWithDocContent).size());
        // assertEquals(1, nodeService.getChildAssocs(nodeWithDocContent).size());
        renditionAssoc = renditionService.getRenditionByName(nodeWithDocContent, compositeQName);
        assertNotNull(renditionAssoc);
        assertFalse(renditionAssoc.isPrimary());
        children = nodeService.getChildAssocs(testTargetFolder);
        assertEquals(1, children.size());
        childAssoc = children.get(0);
        assertEquals(expectedName, childAssoc.getQName());
        assertTrue(childAssoc.isPrimary());

        // Tidy
        nodeService.deleteNode(
                renditionService.getRenditions(nodeWithDocContent).get(0).getChildRef());

        // ================================================= //
        // Path Based Rendition, existing one, same type //
        // ================================================= //

        // Create one of the right type for a plain rendition
        renditionService.render(nodeWithDocContent, rdPlain);
        assertEquals(1, renditionService.getRenditions(nodeWithDocContent).size());
        // assertEquals(1, nodeService.getChildAssocs(nodeWithDocContent).size());
        assertEquals(1, children.size());
        renditionAssoc = renditionService.getRenditionByName(nodeWithDocContent, plainQName);
        assertNotNull(renditionAssoc);

        // Run again, shouldn't change, should re-use the node
        renditionNode = renditionAssoc.getChildRef();
        renditionService.render(nodeWithDocContent, rdPlain);
        assertEquals(1, renditionService.getRenditions(nodeWithDocContent).size());
        // assertEquals(1, nodeService.getChildAssocs(nodeWithDocContent).size());
        renditionAssoc = renditionService.getRenditionByName(nodeWithDocContent, plainQName);
        assertNotNull(renditionAssoc);
        assertFalse(renditionAssoc.isPrimary());
        assertEquals(renditionNode, renditionAssoc.getChildRef());
        children = nodeService.getChildAssocs(testTargetFolder);
        assertEquals(1, children.size());
        childAssoc = children.get(0);
        assertEquals(expectedName, childAssoc.getQName());
        assertTrue(childAssoc.isPrimary());
        assertEquals(renditionNode, childAssoc.getChildRef());

        // Tidy, and re-create for composite
        nodeService.deleteNode(
                renditionService.getRenditions(nodeWithDocContent).get(0).getChildRef());
        renditionService.render(nodeWithDocContent, rdComposite);
        assertEquals(1, renditionService.getRenditions(nodeWithDocContent).size());
        // assertEquals(1, nodeService.getChildAssocs(nodeWithDocContent).size());
        assertEquals(1, children.size());
        renditionAssoc = renditionService.getRenditionByName(nodeWithDocContent, compositeQName);
        assertNotNull(renditionAssoc);

        // Run again, shouldn't change, should re-use the node
        renditionNode = renditionAssoc.getChildRef();
        renditionService.render(nodeWithDocContent, rdComposite);
        assertEquals(1, renditionService.getRenditions(nodeWithDocContent).size());
        // assertEquals(1, nodeService.getChildAssocs(nodeWithDocContent).size());
        renditionAssoc = renditionService.getRenditionByName(nodeWithDocContent, compositeQName);
        assertNotNull(renditionAssoc);
        assertFalse(renditionAssoc.isPrimary());
        assertEquals(renditionNode, renditionAssoc.getChildRef());
        children = nodeService.getChildAssocs(testTargetFolder);
        assertEquals(1, children.size());
        childAssoc = children.get(0);
        assertEquals(expectedName, childAssoc.getQName());
        assertTrue(childAssoc.isPrimary());
        assertEquals(renditionNode, childAssoc.getChildRef());

        // ================================================= //
        // Path Based Rendition, existing one, wrong type //
        // ================================================= //

        // We currently have a composite one
        assertEquals(expectedName, children.get(0).getQName());

        // Run the plain rendition, the composite one should be replaced
        // with the new one
        renditionNode = children.get(0).getChildRef();

        // Currently, there is only one scenario in which it is legal for a rendition to overwrite an existing
        // node. That is when the rendition is an update of the source node i.e. the rendition node is already
        // linked to its source node by a rn:rendition association of the same name as the new rendition.
        try
        {
            renditionService.render(nodeWithDocContent, rdPlain);
            fail("Expected RenditionServiceException not thrown");
        }
        catch (RenditionServiceException expected)
        {
            // Do Nothing.
        }

    }

    private RenditionDefinition loadAndValidateRenditionDefinition(String renditionLocalName)
    {
        QName renditionQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, renditionLocalName);
        RenditionDefinition renditionDefinition = renditionService.loadRenditionDefinition(renditionQName);
        assertNotNull("'" + renditionLocalName + "' rendition definition was missing.", renditionDefinition);
        assertEquals("'" + renditionLocalName + "' renditionDefinition had wrong renditionName", renditionQName,
                renditionDefinition.getRenditionName());

        assertNotNull("'" + renditionLocalName + "' renditionDefinition had null " +
                RenditionDefinitionImpl.RENDITION_DEFINITION_NAME + " parameter",
                renditionDefinition.getParameterValue(RenditionDefinitionImpl.RENDITION_DEFINITION_NAME));

        // All builtin renditions should be "runas" system
        assertEquals(AuthenticationUtil.getSystemUserName(), renditionDefinition.getParameterValue(AbstractRenderingEngine.PARAM_RUN_AS));

        return renditionDefinition;
    }

    /**
     * Creates a RenderingAction (RenditionDefinition) for the ReformatActionExecutor, setting the mimetype parameter value to plain text.
     * 
     * @param renditionObjectType
     *            requested node type of the rendition object
     * @param targetMimetype
     * @return A new RenderingAction.
     */
    private RenditionDefinition makeReformatAction(QName renditionObjectType, String targetMimetype)
    {
        // Create the rendering action.
        RenditionDefinition action = renditionService.createRenditionDefinition(REFORMAT_RENDER_DEFN_NAME,
                ReformatRenderingEngine.NAME);
        action.setParameterValue(AbstractRenderingEngine.PARAM_MIME_TYPE, targetMimetype);
        if (renditionObjectType != null)
        {
            action.setParameterValue(RenditionService.PARAM_RENDITION_NODETYPE, renditionObjectType);
        }
        return action;
    }

    /**
     * Creates a RenderingAction (RenditionDefinition) for the RescaleImageActionExecutor.
     * 
     * @return A new RenderingAction.
     */
    private RenditionDefinition makeRescaleImageAction()
    {
        RenditionDefinition action = renditionService.createRenditionDefinition(RESCALE_RENDER_DEFN_NAME,
                ImageRenderingEngine.NAME);
        action.setParameterValue(ImageRenderingEngine.PARAM_RESIZE_WIDTH, 42);
        return action;
    }

    private void validateRenderingActionDefinition(final String renderingEngineName)
    {
        // Make sure we can get the action definition.
        RenderingEngineDefinition renderingActionDefn = renditionService
                .getRenderingEngineDefinition(renderingEngineName);
        assertNotNull("renderingActionDefn was null", renderingActionDefn);
        assertEquals("Incorrect renderingActionDefn name", renderingEngineName, renderingActionDefn.getName());
    }

    private void validateRenditionAssociation(ChildAssociationRef chAssRef, QName renderingActionQName)
    {
        assertEquals("The assoc type name was wrong", RenditionModel.ASSOC_RENDITION, chAssRef.getTypeQName());
        assertEquals("The assoc name was wrong", renderingActionQName, chAssRef.getQName());

        assertTrue("The source node should have the rn:renditioned aspect applied", nodeService.hasAspect(chAssRef
                .getParentRef(), RenditionModel.ASPECT_RENDITIONED));

        final NodeRef newRenditionNodeRef = chAssRef.getChildRef();
        assertTrue("The new rendition node was not a rendition.", renditionService.isRendition(newRenditionNodeRef));

        // If the source node for the rendition equals the primary parent
        NodeRef renditionSource = renditionService.getSourceNode(newRenditionNodeRef).getParentRef();
        NodeRef renditionPrimaryParent = nodeService.getPrimaryParent(newRenditionNodeRef).getParentRef();
        if (renditionSource.equals(renditionPrimaryParent))
        {
            assertTrue("Rendition node was missing the hiddenRendition aspect", nodeService.hasAspect(
                    newRenditionNodeRef, RenditionModel.ASPECT_HIDDEN_RENDITION));
        }
        else
        {
            assertTrue("Rendition node was missing the visibleRendition aspect", nodeService.hasAspect(
                    newRenditionNodeRef, RenditionModel.ASPECT_VISIBLE_RENDITION));
        }

        assertEquals(ContentModel.PROP_CONTENT, nodeService.getProperty(newRenditionNodeRef,
                ContentModel.PROP_CONTENT_PROPERTY_NAME));
    }

    /**
     * Given a QName this method returns the long-form String with the braces escaped.
     */
    private String getLongNameWithEscapedBraces(QName qn)
    {
        String longName = qn.toString();
        String escapedBraces = longName.replace("{", "\\{").replace("}", "\\}");
        return escapedBraces;
    }

    private String readTextContent(NodeRef nodeRef)
    {
        ContentReader reader = contentService.getReader(nodeRef, ContentModel.PROP_CONTENT);
        assertNotNull("reader was null", reader);
        reader.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN);
        return reader.getContentString();
    }

    private CompositeRenditionDefinition makeCompositeReformatAndResizeDefinition(final QName renditionName,
            final int newX, final int newY)
    {
        CompositeRenditionDefinition compositeDefinition = renditionService
                .createCompositeRenditionDefinition(renditionName);
        RenditionDefinition reformatDefinition = makeReformatAction(null, MimetypeMap.MIMETYPE_IMAGE_JPEG);
        RenditionDefinition rescaleImageDefinition = makeRescaleImageAction();
        rescaleImageDefinition.setParameterValue(ImageRenderingEngine.PARAM_RESIZE_WIDTH, newX);
        rescaleImageDefinition.setParameterValue(ImageRenderingEngine.PARAM_RESIZE_HEIGHT, newY);

        compositeDefinition.addAction(reformatDefinition);
        compositeDefinition.addAction(rescaleImageDefinition);
        return compositeDefinition;
    }

    @Test
    public void testJavascriptAPI() throws Exception
    {
        Map<String, Object> model = new HashMap<String, Object>();
        model.put("testSourceNode", this.nodeWithImageContent);
        model.put("testDocNode", this.nodeWithDocContent);

        ScriptLocation location = new ClasspathScriptLocation("org/alfresco/repo/rendition/script/test_renditionService.js");
        this.scriptService.executeScript(location, model);
    }

    /**
     * This test method takes an image with Exif Orientation information and checks if it is automatially rotated.
     */
    @Test
    public void testAutoRotateImage() throws Exception
    {
        NodeRef companyHome = repositoryHelper.getCompanyHome();

        // Check image is there
        String imageSource = "images/rotated_gray21.512.jpg";
        File imageFile = retrieveValidImageFile(imageSource);

        // Create node and save contents
        final NodeRef newNodeForRotate = createNode(companyHome, "rotateImageNode", ContentModel.TYPE_CONTENT);
        setImageContentOnNode(newNodeForRotate, MimetypeMap.MIMETYPE_IMAGE_JPEG, imageFile);

        // Test auto rotates
        final Map<String, Serializable> parameterValues = new HashMap<String, Serializable>();
        resizeImageAndCheckOrientation(newNodeForRotate, parameterValues, BLACK, WHITE);

        // Test doesn't auto rotate
        parameterValues.clear();
        parameterValues.put(ImageRenderingEngine.PARAM_AUTO_ORIENTATION, false);
        resizeImageAndCheckOrientation(newNodeForRotate, parameterValues, WHITE, BLACK);

        // Clean up
        nodeService.deleteNode(newNodeForRotate);
    }

    private void resizeImageAndCheckOrientation(final NodeRef nodeToResize, final Map<String, Serializable> parameterValues, final String topLeft, final String bottomRight)
    {
        // Resize to 100 by 100
        final Integer imageNewXSize = Integer.valueOf(100);
        final Integer imageNewYSize = Integer.valueOf(100);
        parameterValues.put(ImageRenderingEngine.PARAM_RESIZE_WIDTH, imageNewXSize);
        parameterValues.put(ImageRenderingEngine.PARAM_RESIZE_HEIGHT, imageNewYSize);

        final NodeRef newRenditionNode = performImageRendition(parameterValues, nodeToResize);

        // Assert that the rendition is of the correct size and has reasonable content.
        transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback<Void>() {
            public Void execute() throws Throwable
            {
                // The rescaled image rendition is a child of the original test
                // node.
                List<ChildAssociationRef> children = nodeService.getChildAssocs(nodeToResize,
                        new RegexQNamePattern(getLongNameWithEscapedBraces(RenditionModel.ASSOC_RENDITION)),
                        new RegexQNamePattern(getLongNameWithEscapedBraces(RESCALE_RENDER_DEFN_NAME)));

                // There should only be one child of the image node: the
                // rendition we've just created.
                assertEquals("Unexpected number of children", 1, children.size());

                NodeRef newImageRendition = children.get(0).getChildRef();
                assertEquals(newRenditionNode, newImageRendition);

                ContentReader reader = contentService.getReader(newImageRendition, ContentModel.PROP_CONTENT);
                assertNotNull("Reader to rendered image was null", reader);
                BufferedImage srcImg = ImageIO.read(reader.getContentInputStream());
                checkTopLeftBottomRight(srcImg, topLeft, bottomRight, "rendered image");

                return null;
            }
        });
    }

    private static File retrieveValidImageFile(String imageSource) throws IOException
    {
        URL url = RenditionServiceIntegrationTest.class.getClassLoader().getResource(imageSource);
        File imageFile = new File(url.getFile());
        BufferedImage img = ImageIO.read(url);
        assertNotNull("image was null", img);
        checkTopLeftBottomRight(img, WHITE, BLACK, "original image");
        return imageFile;
    }

    private static void checkTopLeftBottomRight(BufferedImage img, String topLeft, String bottomRight, String srcImage)
    {
        String rgbAtTopLeft = Integer.toHexString(img.getRGB(1, 1));
        assertTrue("upper left " + rgbAtTopLeft + " should end with " + topLeft + " " + srcImage, rgbAtTopLeft.endsWith(topLeft));
        String rgbAtBottomRight = Integer.toHexString(img.getRGB(img.getWidth() - 1, img.getHeight() - 1));
        assertTrue("lower right " + rgbAtBottomRight + " should end with " + bottomRight + " " + srcImage, rgbAtBottomRight.endsWith(bottomRight));
    }

    /**
     * A dummy rendering engine used in testing
     */
    private static class DummyHelloWorldRenditionEngine extends AbstractRenderingEngine
    {
        private static final String ENGINE_NAME = "helloWorldRenderingEngine";

        /**
         * Loads this executor into the ApplicationContext, if it isn't already there
         */
        public static void registerIfNeeded(ApplicationContext ctx)
        {
            if (!ctx.containsBean(ENGINE_NAME))
            {
                // Create, and do dependencies
                DummyHelloWorldRenditionEngine hw = new DummyHelloWorldRenditionEngine();
                hw.setRuntimeActionService(
                        (RuntimeActionService) ctx.getBean("actionService"));
                hw.setNodeService(
                        (NodeService) ctx.getBean("NodeService"));
                hw.setContentService(
                        (ContentService) ctx.getBean("ContentService"));
                hw.setRenditionService(
                        (RenditionService) ctx.getBean("RenditionService"));
                hw.setBehaviourFilter(
                        (BehaviourFilter) ctx.getBean("policyBehaviourFilter"));
                hw.setRenditionLocationResolver(
                        (RenditionLocationResolver) ctx.getBean("renditionLocationResolver"));

                // Register
                ((ConfigurableApplicationContext) ctx).getBeanFactory().registerSingleton(
                        ENGINE_NAME, hw);
                hw.init();
            }
        }

        @Override
        protected void render(RenderingContext context)
        {
            ContentWriter contentWriter = context.makeContentWriter();
            contentWriter.setMimetype("text/plain");
            contentWriter.putContent("Hello, world!");
        }
    }
}
